diff --git a/.gitignore b/.gitignore index 254d4ae..0108897 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,9 @@ -*.pyc -__pycache__/ -*.h5 -tmp_files/ -*.ipynb -logs/ -envs/ -hidden.py +*.pyc +__pycache__/ +*.h5 +tmp_files/ +*.ipynb +logs/ +envs/ +hidden.py output_files/ \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c38d206..83bb132 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,12 +1,12 @@ -pages: - stage: deploy - script: - - echo "Deploying pre-built HTML..." - - cp -r docs/build/html public # Copy the pre-built HTML to the public directory - artifacts: - paths: - - public - only: - changes: - - docs/source/** # Run only if files in docs/source/ change +pages: + stage: deploy + script: + - echo "Deploying pre-built HTML..." + - cp -r docs/build/html public # Copy the pre-built HTML to the public directory + artifacts: + paths: + - public + only: + changes: + - docs/source/** # Run only if files in docs/source/ change - docs/Makefile \ No newline at end of file diff --git a/README.md b/README.md index 62f171c..5632774 100644 --- a/README.md +++ b/README.md @@ -1,267 +1,267 @@ -## DIMA: Data Integration and Metadata Annotation - - -## Description - -**DIMA** (Data Integration and Metadata Annotation) is a Python package developed to support the findable, accessible, interoperable, and reusable (FAIR) data transformation of multi-instrument data at the **Laboratory of Atmospheric Chemistry** as part of the project **IVDAV**: *Instant and Versatile Data Visualization During the Current Dark Period of the Life Cycle of FAIR Research*, funded by the [ETH-Domain ORD Program Measure 1](https://ethrat.ch/en/measure-1-calls-for-field-specific-actions/). - - -The **FAIR** data transformation involves cycles of data harmonization and metadata review. DIMA facilitates these processes by enabling the integration and annotation of multi-instrument data in HDF5 format. This data may originate from diverse experimental campaigns, including **beamtimes**, **kinetic flowtube studies**, **smog chamber experiments**, and **field campaigns**. - - -## Key features - -DIMA provides reusable operations for data integration, manipulation, and extraction using HDF5 files. These serve as the foundation for the following higher-level operations: - -1. **Data integration pipeline**: Searches for, retrieves, and integrates multi-instrument data sources in HDF5 format using a human-readable campaign descriptor YAML file that points to the data sources on a network drive. - -2. **Metadata revision pipeline**: Enables updates, deletions, and additions of metadata in an HDF5 file. It operates on the target HDF5 file and a YAML file specifying the required changes. A suitable YAML file specification can be generated by serializing the current metadata of the target HDF5 file. This supports alignment with conventions and the development of campaign-centric vocabularies. - - -3. **Visualization pipeline:** - Generates a treemap visualization of an HDF5 file, highlighting its structure and key metadata elements. - -4. **Jupyter notebooks** - Demonstrates DIMA’s core functionalities, such as data integration, HDF5 file creation, visualization, and metadata annotation. Key notebooks include examples for data sharing, OpenBis ETL, and workflow demos. - -## Requirements - -For **Windows** users, the following are required: - -1. **Git Bash**: Install [Git Bash](https://git-scm.com/downloads) to run shell scripts (`.sh` files). - -2. **Conda**: Install [Anaconda](https://www.anaconda.com/products/individual) or [Miniconda](https://docs.conda.io/en/latest/miniconda.html). - -3. **PSI Network Access**: Ensure access to PSI’s network and access rights to source drives for retrieving campaign data from YAML files in the `input_files/` folder. - -:bulb: **Tip**: Editing your system’s PATH variable ensures both Conda and Git are available in the terminal environment used by Git Bash. - - -## Getting Started - -### Download DIMA - -Open a **Git Bash** terminal. - -Navigate to your `GitLab` folder, clone the repository, and navigate to the `dima` folder as follows: - - ```bash - cd path/to/GitLab - git clone --recurse-submodules https://gitlab.psi.ch/5505/dima.git - cd dima - ``` - -### Install Python Interpreter - -Open **Git Bash** terminal. - -**Option 1**: Install a suitable conda environment `multiphase_chemistry_env` inside the repository `dima` as follows: - - ```bash - cd path/to/GitLab/dima - Bash setup_env.sh - ``` - -Open **Anaconda Prompt** or a terminal with access to conda. - -**Option 2**: Install conda enviroment from YAML file as follows: - ```bash - cd path/to/GitLab/dima - conda env create --file environment.yml - ``` - -
- Working with Jupyter Notebooks - -We now make the previously installed Python environment `multiphase_chemistry_env` selectable as a kernel in Jupyter's interface. - -1. Open an Anaconda Prompt, check if the environment exists, and activate it: - ``` - conda env list - conda activate multiphase_chemistry_env - ``` -2. Register the environment in Jupyter: - ``` - python -m ipykernel install --user --name multiphase_chemistry_env --display-name "Python (multiphase_chemistry_env)" - ``` -3. Start a Jupyter Notebook by running the command: - ``` - jupyter notebook - ``` - and select the `multiphase_chemistry_env` environment from the kernel options. - -
- -## Repository Structure and Software arquitecture - -**Directories** - - - `input_files/` stores some example raw input data or campaign descriptor YAML files. - - - `output_files/` stores generated outputs for local processing. - - - `instruments/` contains instrument-specific dictionaries and file readers. - - - `src/` contains the main source code, HDF5 Writer and Data Operations Manager. - - - `utils/` contains generic data conversion operations, supporting the source code. - - - `notebooks/` contains a collection of Jupyter notebooks, demonstrating DIMA's main functionalities. - - - `pipelines/` contains source code for the data integration pipeline and metadata revision workflow. - - - `visualization/` contains primarily functions for visualization of HDF5 files as treemaps. - ---- - -**Software arquitecture** - -

- Alt Text -

- -## File standardization module (`instruments/`) - -### Extend DIMA’s file reading capabilities for new instruments - -We now explain how to extend DIMA's file-reading capabilities by adding support for a new instrument. The process involves adding instrument-specific files and registering the new instrument's file reader. - -1. Create Instrument Files -You need to add two files for the new instrument: - - - A **YAML file** that contains the instrument-specific description terms. - - **Location**: `instruments/dictionaries/` - - - A **Python file** that reads the instrument's data files (e.g., JSON files). - - **Location**: `instruments/readers/` - - **Example:** - - **YAML file**: `ACSM_TOFWARE_flags.yaml` - - **Python file**: `flag_reader.py` (reads `flag.json` files from the new instrument). - -2. Register the New Instrument Reader -To enable DIMA to recognize the new instrument's file reader, update the **filereader registry**: - -1. Open the file: `instruments/readers/filereader_registry.py`. -2. Add an entry to register the new instrument's reader. - -**Example:** -```python -# Import the new reader -from instruments.readers.flag_reader import read_jsonflag_as_dict -# Register the new instrument in the registry -file_extensions.append('.json') -file_readers.update({'ACSM_TOFWARE_flags_json' : lambda x: read_jsonflag_as_dict(x)}) -``` -*** - -## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. - -## License -For open source projects, say how it is licensed. - -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. - -## How-to tutorials - -
- - Data integration workflow - -This section is in progress! - -
- - -
- - Metadata review workflow - -- review through branches -- updating files with metadata in Openbis - -#### Metadata -| **Attribute** | **CF Equivalent** | **Definition** | -|-------------------------|-----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| campaign_name | - | Denotes a range of possible campaigns, including laboratory and field experiments, beamtime, smog chamber studies, etc., related to atmospheric chemistry research. | -| project | - | Denotes a valid name of the project under which the data was collected or produced. | -| contact | contact (specifically E-mail address) | Denotes the name of data producer who conducted the experiment or carried out the project that produced the raw dataset (or an aggragated dataset with multiple owners) | -| description | title (only info about content), comment (too broad in scope), source | Provides a short description of methods and processing steps used to arrive at the current version of the dataset. | -| experiment | - | Denotes a valid name of the specific experiment or study that generated the data. | -| actris_level | - | Indicates the processing level of the data within the ACTRIS (Aerosol, Clouds and Trace Gases Research Infrastructure) framework. | -| dataset_startdate | - | Denotes the start datetime of the dataset collection. | -| dataset_enddate | - | Denotes the end datetime of the dataset collection. | -| processing_file | - | Denotes the name of the file used to process an initial version (e.g, original version) of the dataset into a processed dataset. | -| processing_date | - | The date when the data processing was completed. | | -## Adaptability to Experimental Campaign Needs - -The `instruments/` module is designed to be highly adaptable, accommodating new instrument types or file reading capabilities with minimal code refactoring. The module is complemented by instrument-specific dictionaries of terms in YAML format, which facilitate automated annotation of observed variables with: - - `standard_name` - - `units` - - `description` - - as suggested by [CF metadata conventions](http://cfconventions.org/). -### Versioning and Community Collaboration - The instrument-specific dictionaries in YAML format provide a human readable interface for community-based development of instrument vocabularies. These descriptions can potentially be enhanced with semantic annotations for interoperability across research domains. - -### Specifying a compound attribute in yaml language. -Consider the compound attribute *relative_humidity*, which has subattributes *value*, *units*, *range*, and *definition*. The yaml description of -such an attribute is as follows: -```yaml -relative_humidity: - value: 65 - units: percentage - range: '[0,100]' - definition: 'Relative humidity represents the amount of water vapor present in the air relative to the maximum amount of water vapor the air can hold at a given temperature.' -``` -### Deleting or renaming a compound attribute in yaml language. - - Assume the attribute *relative_humidity* already exists. Then it should be displayed as follows with the subattribute *rename_as*. This can be set differently to suggest a renaming of the attribute. - - To suggest deletion of an attribute, we are required to add a subattribute *delete* with value as *true*. Below for example, the - attribute *relative_ humidity* is suggested to be deleted. Otherwise if *delete* is set as *false*, it will have no effect. -```yaml -relative_humidity: - delete: true # we added this line in the review process - rename_as: relative_humidity - value: 65 - units: percentage - range: '[0,100]' - definition: 'Relative humidity represents the amount of water vapor present in the air relative to the maximum amount of water vapor the air can hold at a given temperature.' - -``` -
- -# Editing this README - -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template. - -## Suggestions for a good README -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. - -## Badges -On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. - -## Visuals -Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. - -## Installation -Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. - -## Usage -Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. - -## Support -Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. - -## Roadmap -If you have ideas for releases in the future, it is a good idea to list them in the README. - -## Contributing -State if you are open to contributions and what your requirements are for accepting them. - -For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. - -You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. - - +## DIMA: Data Integration and Metadata Annotation + + +## Description + +**DIMA** (Data Integration and Metadata Annotation) is a Python package developed to support the findable, accessible, interoperable, and reusable (FAIR) data transformation of multi-instrument data at the **Laboratory of Atmospheric Chemistry** as part of the project **IVDAV**: *Instant and Versatile Data Visualization During the Current Dark Period of the Life Cycle of FAIR Research*, funded by the [ETH-Domain ORD Program Measure 1](https://ethrat.ch/en/measure-1-calls-for-field-specific-actions/). + + +The **FAIR** data transformation involves cycles of data harmonization and metadata review. DIMA facilitates these processes by enabling the integration and annotation of multi-instrument data in HDF5 format. This data may originate from diverse experimental campaigns, including **beamtimes**, **kinetic flowtube studies**, **smog chamber experiments**, and **field campaigns**. + + +## Key features + +DIMA provides reusable operations for data integration, manipulation, and extraction using HDF5 files. These serve as the foundation for the following higher-level operations: + +1. **Data integration pipeline**: Searches for, retrieves, and integrates multi-instrument data sources in HDF5 format using a human-readable campaign descriptor YAML file that points to the data sources on a network drive. + +2. **Metadata revision pipeline**: Enables updates, deletions, and additions of metadata in an HDF5 file. It operates on the target HDF5 file and a YAML file specifying the required changes. A suitable YAML file specification can be generated by serializing the current metadata of the target HDF5 file. This supports alignment with conventions and the development of campaign-centric vocabularies. + + +3. **Visualization pipeline:** + Generates a treemap visualization of an HDF5 file, highlighting its structure and key metadata elements. + +4. **Jupyter notebooks** + Demonstrates DIMA’s core functionalities, such as data integration, HDF5 file creation, visualization, and metadata annotation. Key notebooks include examples for data sharing, OpenBis ETL, and workflow demos. + +## Requirements + +For **Windows** users, the following are required: + +1. **Git Bash**: Install [Git Bash](https://git-scm.com/downloads) to run shell scripts (`.sh` files). + +2. **Conda**: Install [Anaconda](https://www.anaconda.com/products/individual) or [Miniconda](https://docs.conda.io/en/latest/miniconda.html). + +3. **PSI Network Access**: Ensure access to PSI’s network and access rights to source drives for retrieving campaign data from YAML files in the `input_files/` folder. + +:bulb: **Tip**: Editing your system’s PATH variable ensures both Conda and Git are available in the terminal environment used by Git Bash. + + +## Getting Started + +### Download DIMA + +Open a **Git Bash** terminal. + +Navigate to your `GitLab` folder, clone the repository, and navigate to the `dima` folder as follows: + + ```bash + cd path/to/GitLab + git clone --recurse-submodules https://gitlab.psi.ch/5505/dima.git + cd dima + ``` + +### Install Python Interpreter + +Open **Git Bash** terminal. + +**Option 1**: Install a suitable conda environment `multiphase_chemistry_env` inside the repository `dima` as follows: + + ```bash + cd path/to/GitLab/dima + Bash setup_env.sh + ``` + +Open **Anaconda Prompt** or a terminal with access to conda. + +**Option 2**: Install conda enviroment from YAML file as follows: + ```bash + cd path/to/GitLab/dima + conda env create --file environment.yml + ``` + +
+ Working with Jupyter Notebooks + +We now make the previously installed Python environment `multiphase_chemistry_env` selectable as a kernel in Jupyter's interface. + +1. Open an Anaconda Prompt, check if the environment exists, and activate it: + ``` + conda env list + conda activate multiphase_chemistry_env + ``` +2. Register the environment in Jupyter: + ``` + python -m ipykernel install --user --name multiphase_chemistry_env --display-name "Python (multiphase_chemistry_env)" + ``` +3. Start a Jupyter Notebook by running the command: + ``` + jupyter notebook + ``` + and select the `multiphase_chemistry_env` environment from the kernel options. + +
+ +## Repository Structure and Software arquitecture + +**Directories** + + - `input_files/` stores some example raw input data or campaign descriptor YAML files. + + - `output_files/` stores generated outputs for local processing. + + - `instruments/` contains instrument-specific dictionaries and file readers. + + - `src/` contains the main source code, HDF5 Writer and Data Operations Manager. + + - `utils/` contains generic data conversion operations, supporting the source code. + + - `notebooks/` contains a collection of Jupyter notebooks, demonstrating DIMA's main functionalities. + + - `pipelines/` contains source code for the data integration pipeline and metadata revision workflow. + + - `visualization/` contains primarily functions for visualization of HDF5 files as treemaps. + +--- + +**Software arquitecture** + +

+ Alt Text +

+ +## File standardization module (`instruments/`) + +### Extend DIMA’s file reading capabilities for new instruments + +We now explain how to extend DIMA's file-reading capabilities by adding support for a new instrument. The process involves adding instrument-specific files and registering the new instrument's file reader. + +1. Create Instrument Files +You need to add two files for the new instrument: + + - A **YAML file** that contains the instrument-specific description terms. + - **Location**: `instruments/dictionaries/` + + - A **Python file** that reads the instrument's data files (e.g., JSON files). + - **Location**: `instruments/readers/` + + **Example:** + - **YAML file**: `ACSM_TOFWARE_flags.yaml` + - **Python file**: `flag_reader.py` (reads `flag.json` files from the new instrument). + +2. Register the New Instrument Reader +To enable DIMA to recognize the new instrument's file reader, update the **filereader registry**: + +1. Open the file: `instruments/readers/filereader_registry.py`. +2. Add an entry to register the new instrument's reader. + +**Example:** +```python +# Import the new reader +from instruments.readers.flag_reader import read_jsonflag_as_dict +# Register the new instrument in the registry +file_extensions.append('.json') +file_readers.update({'ACSM_TOFWARE_flags_json' : lambda x: read_jsonflag_as_dict(x)}) +``` +*** + +## Authors and acknowledgment +Show your appreciation to those who have contributed to the project. + +## License +For open source projects, say how it is licensed. + +## Project status +If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. + +## How-to tutorials + +
+ + Data integration workflow + +This section is in progress! + +
+ + +
+ + Metadata review workflow + +- review through branches +- updating files with metadata in Openbis + +#### Metadata +| **Attribute** | **CF Equivalent** | **Definition** | +|-------------------------|-----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| campaign_name | - | Denotes a range of possible campaigns, including laboratory and field experiments, beamtime, smog chamber studies, etc., related to atmospheric chemistry research. | +| project | - | Denotes a valid name of the project under which the data was collected or produced. | +| contact | contact (specifically E-mail address) | Denotes the name of data producer who conducted the experiment or carried out the project that produced the raw dataset (or an aggragated dataset with multiple owners) | +| description | title (only info about content), comment (too broad in scope), source | Provides a short description of methods and processing steps used to arrive at the current version of the dataset. | +| experiment | - | Denotes a valid name of the specific experiment or study that generated the data. | +| actris_level | - | Indicates the processing level of the data within the ACTRIS (Aerosol, Clouds and Trace Gases Research Infrastructure) framework. | +| dataset_startdate | - | Denotes the start datetime of the dataset collection. | +| dataset_enddate | - | Denotes the end datetime of the dataset collection. | +| processing_file | - | Denotes the name of the file used to process an initial version (e.g, original version) of the dataset into a processed dataset. | +| processing_date | - | The date when the data processing was completed. | | +## Adaptability to Experimental Campaign Needs + +The `instruments/` module is designed to be highly adaptable, accommodating new instrument types or file reading capabilities with minimal code refactoring. The module is complemented by instrument-specific dictionaries of terms in YAML format, which facilitate automated annotation of observed variables with: + - `standard_name` + - `units` + - `description` + + as suggested by [CF metadata conventions](http://cfconventions.org/). +### Versioning and Community Collaboration + The instrument-specific dictionaries in YAML format provide a human readable interface for community-based development of instrument vocabularies. These descriptions can potentially be enhanced with semantic annotations for interoperability across research domains. + +### Specifying a compound attribute in yaml language. +Consider the compound attribute *relative_humidity*, which has subattributes *value*, *units*, *range*, and *definition*. The yaml description of +such an attribute is as follows: +```yaml +relative_humidity: + value: 65 + units: percentage + range: '[0,100]' + definition: 'Relative humidity represents the amount of water vapor present in the air relative to the maximum amount of water vapor the air can hold at a given temperature.' +``` +### Deleting or renaming a compound attribute in yaml language. + - Assume the attribute *relative_humidity* already exists. Then it should be displayed as follows with the subattribute *rename_as*. This can be set differently to suggest a renaming of the attribute. + - To suggest deletion of an attribute, we are required to add a subattribute *delete* with value as *true*. Below for example, the + attribute *relative_ humidity* is suggested to be deleted. Otherwise if *delete* is set as *false*, it will have no effect. +```yaml +relative_humidity: + delete: true # we added this line in the review process + rename_as: relative_humidity + value: 65 + units: percentage + range: '[0,100]' + definition: 'Relative humidity represents the amount of water vapor present in the air relative to the maximum amount of water vapor the air can hold at a given temperature.' + +``` +
+ +# Editing this README + +When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template. + +## Suggestions for a good README +Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. + +## Badges +On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. + +## Visuals +Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. + +## Installation +Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. + +## Usage +Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. + +## Support +Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. + +## Roadmap +If you have ideas for releases in the future, it is a good idea to list them in the README. + +## Contributing +State if you are open to contributions and what your requirements are for accepting them. + +For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. + +You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. + + diff --git a/TODO.md b/TODO.md index 8e5e71b..b5abc87 100644 --- a/TODO.md +++ b/TODO.md @@ -1,3 +1,3 @@ -## TODO -* Improve file reader for rga txt files (Talk to Thorsten). Example folder contains incosistent rga txt files, there is no unique column separator (, or ). -* Improve documentation of file reader extensions. Specify PY file reader template with expect input and output and file naming convention. +## TODO +* Improve file reader for rga txt files (Talk to Thorsten). Example folder contains incosistent rga txt files, there is no unique column separator (, or ). +* Improve documentation of file reader extensions. Specify PY file reader template with expect input and output and file naming convention. diff --git a/docs/Makefile b/docs/Makefile index d0c3cbf..26b9422 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,20 +1,20 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = source -BUILDDIR = build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/build/html/.buildinfo b/docs/build/html/.buildinfo index e89a8bb..f4bce1a 100644 --- a/docs/build/html/.buildinfo +++ b/docs/build/html/.buildinfo @@ -1,4 +1,4 @@ -# Sphinx build info version 1 -# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: 19124d911d3357bfa2d9f6131966b4c9 -tags: 645f666f9bcd5a90fca523b33c5a78b7 +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 19124d911d3357bfa2d9f6131966b4c9 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/build/html/_modules/index.html b/docs/build/html/_modules/index.html index e125ff0..970cc58 100644 --- a/docs/build/html/_modules/index.html +++ b/docs/build/html/_modules/index.html @@ -1,114 +1,114 @@ - - - - - - Overview: module code — DIMA 1.0.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - -
  • -
  • -
-
-
- - -
-
-
-
- - - + + + + + + Overview: module code — DIMA 1.0.0 documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+ + +
+
+
+
+ + + \ No newline at end of file diff --git a/docs/build/html/_modules/pipelines/data_integration.html b/docs/build/html/_modules/pipelines/data_integration.html index 1a266a8..6328cab 100644 --- a/docs/build/html/_modules/pipelines/data_integration.html +++ b/docs/build/html/_modules/pipelines/data_integration.html @@ -1,359 +1,359 @@ - - - - - - pipelines.data_integration — DIMA 1.0.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for pipelines.data_integration

-import sys
-import os
-
-try:
-    thisFilePath = os.path.abspath(__file__)
-except NameError:
-    print("Error: __file__ is not available. Ensure the script is being run from a file.")
-    print("[Notice] Path to DIMA package may not be resolved properly.")
-    thisFilePath = os.getcwd()  # Use current directory or specify a default
-
-dimaPath = os.path.normpath(os.path.join(thisFilePath, "..",'..'))  # Move up to project root
-
-if dimaPath not in sys.path:  # Avoid duplicate entries
-    sys.path.append(dimaPath)
-
-
-import yaml 
-import logging
-from datetime import datetime
-# Importing chain class from itertools 
-from itertools import chain 
-
-# Import DIMA modules
-import src.hdf5_writer as hdf5_lib
-import utils.g5505_utils as utils
-from instruments.readers import filereader_registry
-
-allowed_file_extensions = filereader_registry.file_extensions
-
-def _generate_datetime_dict(datetime_steps):
-    """ Generate the datetime augment dictionary from datetime steps. """
-    datetime_augment_dict = {}
-    for datetime_step in datetime_steps:
-        #tmp = datetime.strptime(datetime_step, '%Y-%m-%d %H-%M-%S')
-        datetime_augment_dict[datetime_step] = [
-            datetime_step.strftime('%Y-%m-%d'), datetime_step.strftime('%Y_%m_%d'), datetime_step.strftime('%Y.%m.%d'), datetime_step.strftime('%Y%m%d')
-        ]
-    return datetime_augment_dict
-
-
-[docs] -def load_config_and_setup_logging(yaml_config_file_path, log_dir): - """Load YAML configuration file, set up logging, and validate required keys and datetime_steps.""" - - # Define required keys - required_keys = [ - 'experiment', 'contact', 'input_file_directory', 'output_file_directory', - 'instrument_datafolder', 'project', 'actris_level' - ] - - # Supported integration modes - supported_integration_modes = ['collection', 'single_experiment'] - - - # Set up logging - date = utils.created_at("%Y_%m").replace(":", "-") - utils.setup_logging(log_dir, f"integrate_data_sources_{date}.log") - - # Load YAML configuration file - with open(yaml_config_file_path, 'r') as stream: - try: - config_dict = yaml.load(stream, Loader=yaml.FullLoader) - except yaml.YAMLError as exc: - logging.error("Error loading YAML file: %s", exc) - raise ValueError(f"Failed to load YAML file: {exc}") - - # Check if required keys are present - missing_keys = [key for key in required_keys if key not in config_dict] - if missing_keys: - raise KeyError(f"Missing required keys in YAML configuration: {missing_keys}") - - # Validate integration_mode - integration_mode = config_dict.get('integration_mode', 'N/A') # Default to 'collection' - if integration_mode not in supported_integration_modes: - raise RuntimeWarning( - f"Unsupported integration_mode '{integration_mode}'. Supported modes are {supported_integration_modes}. Setting '{integration_mode}' to 'single_experiment'." - ) - - - # Validate datetime_steps format if it exists - if 'datetime_steps' in config_dict: - datetime_steps = config_dict['datetime_steps'] - expected_format = '%Y-%m-%d %H-%M-%S' - - # Check if datetime_steps is a list or a falsy value - if datetime_steps and not isinstance(datetime_steps, list): - raise TypeError(f"datetime_steps should be a list of strings or a falsy value (None, empty), but got {type(datetime_steps)}") - - for step_idx, step in enumerate(datetime_steps): - try: - # Attempt to parse the datetime to ensure correct format - config_dict['datetime_steps'][step_idx] = datetime.strptime(step, expected_format) - except ValueError: - raise ValueError(f"Invalid datetime format for '{step}'. Expected format: {expected_format}") - # Augment datatime_steps list as a dictionary. This to speed up single-experiment file generation - config_dict['datetime_steps_dict'] = _generate_datetime_dict(datetime_steps) - else: - # If datetime_steps is not present, set the integration mode to 'collection' - logging.info("datetime_steps missing, setting integration_mode to 'collection'.") - config_dict['integration_mode'] = 'collection' - - # Validate filename_format if defined - if 'filename_format' in config_dict: - if not isinstance(config_dict['filename_format'], str): - raise ValueError(f'"Specified filename_format needs to be of String type" ') - - # Split the string and check if each key exists in config_dict - keys = [key.strip() for key in config_dict['filename_format'].split(',')] - missing_keys = [key for key in keys if key not in config_dict] - - # If there are any missing keys, raise an assertion error - # assert not missing_keys, f'Missing key(s) in config_dict: {", ".join(missing_keys)}' - if not missing_keys: - config_dict['filename_format'] = ','.join(keys) - else: - config_dict['filename_format'] = None - print(f'"filename_format" should contain comma-separated keys that match existing keys in the YAML config file.') - print('Setting "filename_format" as None') - else: - config_dict['filename_format'] = None - - # Compute complementary metadata elements - - # Create output filename prefix - if not config_dict['filename_format']: # default behavior - config_dict['filename_prefix'] = '_'.join([config_dict[key] for key in ['experiment', 'contact']]) - else: - config_dict['filename_prefix'] = '_'.join([config_dict[key] for key in config_dict['filename_format'].split(sep=',')]) - - # Set default dates from datetime_steps if not provided - current_date = datetime.now().strftime('%Y-%m-%d') - dates = config_dict.get('datetime_steps',[]) - if not config_dict.get('dataset_startdate'): - config_dict['dataset_startdate'] = min(config_dict['datetime_steps']).strftime('%Y-%m-%d') if dates else current_date # Earliest datetime step - - if not config_dict.get('dataset_enddate'): - config_dict['dataset_enddate'] = max(config_dict['datetime_steps']).strftime('%Y-%m-%d') if dates else current_date # Latest datetime step - - config_dict['expected_datetime_format'] = '%Y-%m-%d %H-%M-%S' - - return config_dict
- - - -
-[docs] -def copy_subtree_and_create_hdf5(src, dst, select_dir_keywords, select_file_keywords, allowed_file_extensions, root_metadata_dict): - - """Helper function to copy directory with constraints and create HDF5.""" - src = src.replace(os.sep,'/') - dst = dst.replace(os.sep,'/') - - logging.info("Creating constrained copy of the experimental campaign folder %s at: %s", src, dst) - - path_to_files_dict = utils.copy_directory_with_contraints(src, dst, select_dir_keywords, select_file_keywords, allowed_file_extensions) - logging.info("Finished creating a copy of the experimental campaign folder tree at: %s", dst) - - - logging.info("Creating HDF5 file at: %s", dst) - hdf5_path = hdf5_lib.create_hdf5_file_from_filesystem_path(dst, path_to_files_dict, select_dir_keywords, root_metadata_dict) - logging.info("Completed creation of HDF5 file %s at: %s", hdf5_path, dst) - - return hdf5_path
- - - -
-[docs] -def run_pipeline(path_to_config_yamlFile, log_dir='logs/'): - - """Integrates data sources specified by the input configuration file into HDF5 files. - - Parameters: - yaml_config_file_path (str): Path to the YAML configuration file. - log_dir (str): Directory to save the log file. - - Returns: - list: List of Paths to the created HDF5 file(s). - """ - - config_dict = load_config_and_setup_logging(path_to_config_yamlFile, log_dir) - - path_to_input_dir = config_dict['input_file_directory'] - path_to_output_dir = config_dict['output_file_directory'] - select_dir_keywords = config_dict['instrument_datafolder'] - - # Define root folder metadata dictionary - root_metadata_dict = {key : config_dict[key] for key in ['project', 'experiment', 'contact', 'actris_level']} - - # Get dataset start and end dates - dataset_startdate = config_dict['dataset_startdate'] - dataset_enddate = config_dict['dataset_enddate'] - - # Determine mode and process accordingly - output_filename_path = [] - campaign_name_template = lambda filename_prefix, suffix: '_'.join([filename_prefix, suffix]) - date_str = f'{dataset_startdate}_{dataset_enddate}' - - # Create path to new raw datafolder and standardize with forward slashes - path_to_rawdata_folder = os.path.join( - path_to_output_dir, 'collection_' + campaign_name_template(config_dict['filename_prefix'], date_str), "").replace(os.sep, '/') - - # Process individual datetime steps if available, regardless of mode - if config_dict.get('datetime_steps_dict', {}): - # Single experiment mode - for datetime_step, file_keywords in config_dict['datetime_steps_dict'].items(): - date_str = datetime_step.strftime('%Y-%m-%d') - single_campaign_name = campaign_name_template(config_dict['filename_prefix'], date_str) - path_to_rawdata_subfolder = os.path.join(path_to_rawdata_folder, single_campaign_name, "") - - path_to_integrated_stepwise_hdf5_file = copy_subtree_and_create_hdf5( - path_to_input_dir, path_to_rawdata_subfolder, select_dir_keywords, - file_keywords, allowed_file_extensions, root_metadata_dict) - - output_filename_path.append(path_to_integrated_stepwise_hdf5_file) - - # Collection mode processing if specified - if 'collection' in config_dict.get('integration_mode', 'single_experiment'): - path_to_filenames_dict = {path_to_rawdata_folder: [os.path.basename(path) for path in output_filename_path]} if output_filename_path else {} - hdf5_path = hdf5_lib.create_hdf5_file_from_filesystem_path(path_to_rawdata_folder, path_to_filenames_dict, [], root_metadata_dict) - output_filename_path.append(hdf5_path) - else: - path_to_integrated_stepwise_hdf5_file = copy_subtree_and_create_hdf5( - path_to_input_dir, path_to_rawdata_folder, select_dir_keywords, [], - allowed_file_extensions, root_metadata_dict) - output_filename_path.append(path_to_integrated_stepwise_hdf5_file) - - return output_filename_path
- - - -if __name__ == "__main__": - - if len(sys.argv) < 2: - print("Usage: python data_integration.py <function_name> <function_args>") - sys.exit(1) - - # Extract the function name from the command line arguments - function_name = sys.argv[1] - - # Handle function execution based on the provided function name - if function_name == 'run': - - if len(sys.argv) != 3: - print("Usage: python data_integration.py run <path_to_config_yamlFile>") - sys.exit(1) - # Extract path to configuration file, specifying the data integration task - path_to_config_yamlFile = sys.argv[2] - run_pipeline(path_to_config_yamlFile) - - -
- -
-
- -
-
-
-
- - - + + + + + + pipelines.data_integration — DIMA 1.0.0 documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for pipelines.data_integration

+import sys
+import os
+
+try:
+    thisFilePath = os.path.abspath(__file__)
+except NameError:
+    print("Error: __file__ is not available. Ensure the script is being run from a file.")
+    print("[Notice] Path to DIMA package may not be resolved properly.")
+    thisFilePath = os.getcwd()  # Use current directory or specify a default
+
+dimaPath = os.path.normpath(os.path.join(thisFilePath, "..",'..'))  # Move up to project root
+
+if dimaPath not in sys.path:  # Avoid duplicate entries
+    sys.path.append(dimaPath)
+
+
+import yaml 
+import logging
+from datetime import datetime
+# Importing chain class from itertools 
+from itertools import chain 
+
+# Import DIMA modules
+import src.hdf5_writer as hdf5_lib
+import utils.g5505_utils as utils
+from instruments.readers import filereader_registry
+
+allowed_file_extensions = filereader_registry.file_extensions
+
+def _generate_datetime_dict(datetime_steps):
+    """ Generate the datetime augment dictionary from datetime steps. """
+    datetime_augment_dict = {}
+    for datetime_step in datetime_steps:
+        #tmp = datetime.strptime(datetime_step, '%Y-%m-%d %H-%M-%S')
+        datetime_augment_dict[datetime_step] = [
+            datetime_step.strftime('%Y-%m-%d'), datetime_step.strftime('%Y_%m_%d'), datetime_step.strftime('%Y.%m.%d'), datetime_step.strftime('%Y%m%d')
+        ]
+    return datetime_augment_dict
+
+
+[docs] +def load_config_and_setup_logging(yaml_config_file_path, log_dir): + """Load YAML configuration file, set up logging, and validate required keys and datetime_steps.""" + + # Define required keys + required_keys = [ + 'experiment', 'contact', 'input_file_directory', 'output_file_directory', + 'instrument_datafolder', 'project', 'actris_level' + ] + + # Supported integration modes + supported_integration_modes = ['collection', 'single_experiment'] + + + # Set up logging + date = utils.created_at("%Y_%m").replace(":", "-") + utils.setup_logging(log_dir, f"integrate_data_sources_{date}.log") + + # Load YAML configuration file + with open(yaml_config_file_path, 'r') as stream: + try: + config_dict = yaml.load(stream, Loader=yaml.FullLoader) + except yaml.YAMLError as exc: + logging.error("Error loading YAML file: %s", exc) + raise ValueError(f"Failed to load YAML file: {exc}") + + # Check if required keys are present + missing_keys = [key for key in required_keys if key not in config_dict] + if missing_keys: + raise KeyError(f"Missing required keys in YAML configuration: {missing_keys}") + + # Validate integration_mode + integration_mode = config_dict.get('integration_mode', 'N/A') # Default to 'collection' + if integration_mode not in supported_integration_modes: + raise RuntimeWarning( + f"Unsupported integration_mode '{integration_mode}'. Supported modes are {supported_integration_modes}. Setting '{integration_mode}' to 'single_experiment'." + ) + + + # Validate datetime_steps format if it exists + if 'datetime_steps' in config_dict: + datetime_steps = config_dict['datetime_steps'] + expected_format = '%Y-%m-%d %H-%M-%S' + + # Check if datetime_steps is a list or a falsy value + if datetime_steps and not isinstance(datetime_steps, list): + raise TypeError(f"datetime_steps should be a list of strings or a falsy value (None, empty), but got {type(datetime_steps)}") + + for step_idx, step in enumerate(datetime_steps): + try: + # Attempt to parse the datetime to ensure correct format + config_dict['datetime_steps'][step_idx] = datetime.strptime(step, expected_format) + except ValueError: + raise ValueError(f"Invalid datetime format for '{step}'. Expected format: {expected_format}") + # Augment datatime_steps list as a dictionary. This to speed up single-experiment file generation + config_dict['datetime_steps_dict'] = _generate_datetime_dict(datetime_steps) + else: + # If datetime_steps is not present, set the integration mode to 'collection' + logging.info("datetime_steps missing, setting integration_mode to 'collection'.") + config_dict['integration_mode'] = 'collection' + + # Validate filename_format if defined + if 'filename_format' in config_dict: + if not isinstance(config_dict['filename_format'], str): + raise ValueError(f'"Specified filename_format needs to be of String type" ') + + # Split the string and check if each key exists in config_dict + keys = [key.strip() for key in config_dict['filename_format'].split(',')] + missing_keys = [key for key in keys if key not in config_dict] + + # If there are any missing keys, raise an assertion error + # assert not missing_keys, f'Missing key(s) in config_dict: {", ".join(missing_keys)}' + if not missing_keys: + config_dict['filename_format'] = ','.join(keys) + else: + config_dict['filename_format'] = None + print(f'"filename_format" should contain comma-separated keys that match existing keys in the YAML config file.') + print('Setting "filename_format" as None') + else: + config_dict['filename_format'] = None + + # Compute complementary metadata elements + + # Create output filename prefix + if not config_dict['filename_format']: # default behavior + config_dict['filename_prefix'] = '_'.join([config_dict[key] for key in ['experiment', 'contact']]) + else: + config_dict['filename_prefix'] = '_'.join([config_dict[key] for key in config_dict['filename_format'].split(sep=',')]) + + # Set default dates from datetime_steps if not provided + current_date = datetime.now().strftime('%Y-%m-%d') + dates = config_dict.get('datetime_steps',[]) + if not config_dict.get('dataset_startdate'): + config_dict['dataset_startdate'] = min(config_dict['datetime_steps']).strftime('%Y-%m-%d') if dates else current_date # Earliest datetime step + + if not config_dict.get('dataset_enddate'): + config_dict['dataset_enddate'] = max(config_dict['datetime_steps']).strftime('%Y-%m-%d') if dates else current_date # Latest datetime step + + config_dict['expected_datetime_format'] = '%Y-%m-%d %H-%M-%S' + + return config_dict
+ + + +
+[docs] +def copy_subtree_and_create_hdf5(src, dst, select_dir_keywords, select_file_keywords, allowed_file_extensions, root_metadata_dict): + + """Helper function to copy directory with constraints and create HDF5.""" + src = src.replace(os.sep,'/') + dst = dst.replace(os.sep,'/') + + logging.info("Creating constrained copy of the experimental campaign folder %s at: %s", src, dst) + + path_to_files_dict = utils.copy_directory_with_contraints(src, dst, select_dir_keywords, select_file_keywords, allowed_file_extensions) + logging.info("Finished creating a copy of the experimental campaign folder tree at: %s", dst) + + + logging.info("Creating HDF5 file at: %s", dst) + hdf5_path = hdf5_lib.create_hdf5_file_from_filesystem_path(dst, path_to_files_dict, select_dir_keywords, root_metadata_dict) + logging.info("Completed creation of HDF5 file %s at: %s", hdf5_path, dst) + + return hdf5_path
+ + + +
+[docs] +def run_pipeline(path_to_config_yamlFile, log_dir='logs/'): + + """Integrates data sources specified by the input configuration file into HDF5 files. + + Parameters: + yaml_config_file_path (str): Path to the YAML configuration file. + log_dir (str): Directory to save the log file. + + Returns: + list: List of Paths to the created HDF5 file(s). + """ + + config_dict = load_config_and_setup_logging(path_to_config_yamlFile, log_dir) + + path_to_input_dir = config_dict['input_file_directory'] + path_to_output_dir = config_dict['output_file_directory'] + select_dir_keywords = config_dict['instrument_datafolder'] + + # Define root folder metadata dictionary + root_metadata_dict = {key : config_dict[key] for key in ['project', 'experiment', 'contact', 'actris_level']} + + # Get dataset start and end dates + dataset_startdate = config_dict['dataset_startdate'] + dataset_enddate = config_dict['dataset_enddate'] + + # Determine mode and process accordingly + output_filename_path = [] + campaign_name_template = lambda filename_prefix, suffix: '_'.join([filename_prefix, suffix]) + date_str = f'{dataset_startdate}_{dataset_enddate}' + + # Create path to new raw datafolder and standardize with forward slashes + path_to_rawdata_folder = os.path.join( + path_to_output_dir, 'collection_' + campaign_name_template(config_dict['filename_prefix'], date_str), "").replace(os.sep, '/') + + # Process individual datetime steps if available, regardless of mode + if config_dict.get('datetime_steps_dict', {}): + # Single experiment mode + for datetime_step, file_keywords in config_dict['datetime_steps_dict'].items(): + date_str = datetime_step.strftime('%Y-%m-%d') + single_campaign_name = campaign_name_template(config_dict['filename_prefix'], date_str) + path_to_rawdata_subfolder = os.path.join(path_to_rawdata_folder, single_campaign_name, "") + + path_to_integrated_stepwise_hdf5_file = copy_subtree_and_create_hdf5( + path_to_input_dir, path_to_rawdata_subfolder, select_dir_keywords, + file_keywords, allowed_file_extensions, root_metadata_dict) + + output_filename_path.append(path_to_integrated_stepwise_hdf5_file) + + # Collection mode processing if specified + if 'collection' in config_dict.get('integration_mode', 'single_experiment'): + path_to_filenames_dict = {path_to_rawdata_folder: [os.path.basename(path) for path in output_filename_path]} if output_filename_path else {} + hdf5_path = hdf5_lib.create_hdf5_file_from_filesystem_path(path_to_rawdata_folder, path_to_filenames_dict, [], root_metadata_dict) + output_filename_path.append(hdf5_path) + else: + path_to_integrated_stepwise_hdf5_file = copy_subtree_and_create_hdf5( + path_to_input_dir, path_to_rawdata_folder, select_dir_keywords, [], + allowed_file_extensions, root_metadata_dict) + output_filename_path.append(path_to_integrated_stepwise_hdf5_file) + + return output_filename_path
+ + + +if __name__ == "__main__": + + if len(sys.argv) < 2: + print("Usage: python data_integration.py <function_name> <function_args>") + sys.exit(1) + + # Extract the function name from the command line arguments + function_name = sys.argv[1] + + # Handle function execution based on the provided function name + if function_name == 'run': + + if len(sys.argv) != 3: + print("Usage: python data_integration.py run <path_to_config_yamlFile>") + sys.exit(1) + # Extract path to configuration file, specifying the data integration task + path_to_config_yamlFile = sys.argv[2] + run_pipeline(path_to_config_yamlFile) + + +
+ +
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/docs/build/html/_modules/pipelines/metadata_revision.html b/docs/build/html/_modules/pipelines/metadata_revision.html index abc1ce5..b2cc4cb 100644 --- a/docs/build/html/_modules/pipelines/metadata_revision.html +++ b/docs/build/html/_modules/pipelines/metadata_revision.html @@ -1,299 +1,299 @@ - - - - - - pipelines.metadata_revision — DIMA 1.0.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for pipelines.metadata_revision

-import sys
-import os
-
-try:
-    thisFilePath = os.path.abspath(__file__)
-except NameError:
-    print("Error: __file__ is not available. Ensure the script is being run from a file.")
-    print("[Notice] Path to DIMA package may not be resolved properly.")
-    thisFilePath = os.getcwd()  # Use current directory or specify a default
-
-dimaPath = os.path.normpath(os.path.join(thisFilePath, "..",'..'))  # Move up to project root
-
-if dimaPath not in sys.path:  # Avoid duplicate entries
-    sys.path.append(dimaPath)
-
-import h5py
-import yaml
-import src.hdf5_ops as hdf5_ops
-
-
-
-[docs] -def load_yaml(review_yaml_file): - with open(review_yaml_file, 'r') as stream: - try: - return yaml.load(stream, Loader=yaml.FullLoader) - except yaml.YAMLError as exc: - print(exc) - return None
- - -
-[docs] -def validate_yaml_dict(input_hdf5_file, yaml_dict): - errors = [] - notes = [] - - with h5py.File(input_hdf5_file, 'r') as hdf5_file: - # 1. Check for valid object names - for key in yaml_dict: - if key not in hdf5_file: - error_msg = f"Error: {key} is not a valid object's name in the HDF5 file." - print(error_msg) - errors.append(error_msg) - - # 2. Confirm metadata dict for each object is a dictionary - for key, meta_dict in yaml_dict.items(): - if not isinstance(meta_dict, dict): - error_msg = f"Error: Metadata for {key} should be a dictionary." - print(error_msg) - errors.append(error_msg) - else: - if 'attributes' not in meta_dict: - warning_msg = f"Warning: No 'attributes' in metadata dict for {key}." - print(warning_msg) - notes.append(warning_msg) - - # 3. Verify update, append, and delete operations are well specified - for key, meta_dict in yaml_dict.items(): - attributes = meta_dict.get("attributes", {}) - - for attr_name, attr_value in attributes.items(): - # Ensure the object exists before accessing attributes - if key in hdf5_file: - hdf5_obj_attrs = hdf5_file[key].attrs # Access object-specific attributes - - if attr_name in hdf5_obj_attrs: - # Attribute exists: it can be updated or deleted - if isinstance(attr_value, dict) and "delete" in attr_value: - note_msg = f"Note: '{attr_name}' in {key} may be deleted if 'delete' is set as true." - print(note_msg) - notes.append(note_msg) - else: - note_msg = f"Note: '{attr_name}' in {key} will be updated." - print(note_msg) - notes.append(note_msg) - else: - # Attribute does not exist: it can be appended or flagged as an invalid delete - if isinstance(attr_value, dict) and "delete" in attr_value: - error_msg = f"Error: Cannot delete non-existent attribute '{attr_name}' in {key}." - print(error_msg) - errors.append(error_msg) - else: - note_msg = f"Note: '{attr_name}' in {key} will be appended." - print(note_msg) - notes.append(note_msg) - else: - error_msg = f"Error: '{key}' is not a valid object in the HDF5 file." - print(error_msg) - errors.append(error_msg) - - return len(errors) == 0, errors, notes
- - - -
-[docs] -def update_hdf5_file_with_review(input_hdf5_file, review_yaml_file): - - """ - Updates, appends, or deletes metadata attributes in an HDF5 file based on a provided YAML dictionary. - - Parameters: - ----------- - input_hdf5_file : str - Path to the HDF5 file. - - yaml_dict : dict - Dictionary specifying objects and their attributes with operations. Example format: - { - "object_name": { "attributes" : "attr_name": { "value": attr_value, - "delete": true | false - } - } - } - """ - yaml_dict = load_yaml(review_yaml_file) - - success, errors, notes = validate_yaml_dict(input_hdf5_file,yaml_dict) - if not success: - raise ValueError(f"Review yaml file {review_yaml_file} is invalid. Validation errors: {errors}") - - # Initialize HDF5 operations manager - DataOpsAPI = hdf5_ops.HDF5DataOpsManager(input_hdf5_file) - DataOpsAPI.load_file_obj() - - # Iterate over each object in the YAML dictionary - for obj_name, attr_dict in yaml_dict.items(): - # Prepare dictionaries for append, update, and delete actions - append_dict = {} - update_dict = {} - delete_dict = {} - - if not obj_name in DataOpsAPI.file_obj: - continue # Skip if the object does not exist - - # Iterate over each attribute in the current object - for attr_name, attr_props in attr_dict['attributes'].items(): - if not isinstance(attr_props, dict): - #attr_props = {'value': attr_props} - # Check if the attribute exists (for updating) - if attr_name in DataOpsAPI.file_obj[obj_name].attrs: - update_dict[attr_name] = attr_props - # Otherwise, it's a new attribute to append - else: - append_dict[attr_name] = attr_props - else: - # Check if the attribute is marked for deletion - if attr_props.get('delete', False): - delete_dict[attr_name] = attr_props - - # Perform a single pass for all three operations - if append_dict: - DataOpsAPI.append_metadata(obj_name, append_dict) - if update_dict: - DataOpsAPI.update_metadata(obj_name, update_dict) - if delete_dict: - DataOpsAPI.delete_metadata(obj_name, delete_dict) - - # Close hdf5 file - DataOpsAPI.unload_file_obj() - # Regenerate yaml snapshot of updated HDF5 file - output_yml_filename_path = hdf5_ops.serialize_metadata(input_hdf5_file) - print(f'{output_yml_filename_path} was successfully regenerated from the updated version of{input_hdf5_file}')
- - -
-[docs] -def count(hdf5_obj,yml_dict): - print(hdf5_obj.name) - if isinstance(hdf5_obj,h5py.Group) and len(hdf5_obj.name.split('/')) <= 4: - obj_review = yml_dict[hdf5_obj.name] - additions = [not (item in hdf5_obj.attrs.keys()) for item in obj_review['attributes'].keys()] - count_additions = sum(additions) - deletions = [not (item in obj_review['attributes'].keys()) for item in hdf5_obj.attrs.keys()] - count_delections = sum(deletions) - print('additions',count_additions, 'deletions', count_delections)
- - -if __name__ == "__main__": - - if len(sys.argv) < 4: - print("Usage: python metadata_revision.py update <path/to/target_file.hdf5> <path/to/metadata_review_file.yaml>") - sys.exit(1) - - - if sys.argv[1] == 'update': - input_hdf5_file = sys.argv[2] - review_yaml_file = sys.argv[3] - update_hdf5_file_with_review(input_hdf5_file, review_yaml_file) - #run(sys.argv[2]) -
- -
-
- -
-
-
-
- - - + + + + + + pipelines.metadata_revision — DIMA 1.0.0 documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for pipelines.metadata_revision

+import sys
+import os
+
+try:
+    thisFilePath = os.path.abspath(__file__)
+except NameError:
+    print("Error: __file__ is not available. Ensure the script is being run from a file.")
+    print("[Notice] Path to DIMA package may not be resolved properly.")
+    thisFilePath = os.getcwd()  # Use current directory or specify a default
+
+dimaPath = os.path.normpath(os.path.join(thisFilePath, "..",'..'))  # Move up to project root
+
+if dimaPath not in sys.path:  # Avoid duplicate entries
+    sys.path.append(dimaPath)
+
+import h5py
+import yaml
+import src.hdf5_ops as hdf5_ops
+
+
+
+[docs] +def load_yaml(review_yaml_file): + with open(review_yaml_file, 'r') as stream: + try: + return yaml.load(stream, Loader=yaml.FullLoader) + except yaml.YAMLError as exc: + print(exc) + return None
+ + +
+[docs] +def validate_yaml_dict(input_hdf5_file, yaml_dict): + errors = [] + notes = [] + + with h5py.File(input_hdf5_file, 'r') as hdf5_file: + # 1. Check for valid object names + for key in yaml_dict: + if key not in hdf5_file: + error_msg = f"Error: {key} is not a valid object's name in the HDF5 file." + print(error_msg) + errors.append(error_msg) + + # 2. Confirm metadata dict for each object is a dictionary + for key, meta_dict in yaml_dict.items(): + if not isinstance(meta_dict, dict): + error_msg = f"Error: Metadata for {key} should be a dictionary." + print(error_msg) + errors.append(error_msg) + else: + if 'attributes' not in meta_dict: + warning_msg = f"Warning: No 'attributes' in metadata dict for {key}." + print(warning_msg) + notes.append(warning_msg) + + # 3. Verify update, append, and delete operations are well specified + for key, meta_dict in yaml_dict.items(): + attributes = meta_dict.get("attributes", {}) + + for attr_name, attr_value in attributes.items(): + # Ensure the object exists before accessing attributes + if key in hdf5_file: + hdf5_obj_attrs = hdf5_file[key].attrs # Access object-specific attributes + + if attr_name in hdf5_obj_attrs: + # Attribute exists: it can be updated or deleted + if isinstance(attr_value, dict) and "delete" in attr_value: + note_msg = f"Note: '{attr_name}' in {key} may be deleted if 'delete' is set as true." + print(note_msg) + notes.append(note_msg) + else: + note_msg = f"Note: '{attr_name}' in {key} will be updated." + print(note_msg) + notes.append(note_msg) + else: + # Attribute does not exist: it can be appended or flagged as an invalid delete + if isinstance(attr_value, dict) and "delete" in attr_value: + error_msg = f"Error: Cannot delete non-existent attribute '{attr_name}' in {key}." + print(error_msg) + errors.append(error_msg) + else: + note_msg = f"Note: '{attr_name}' in {key} will be appended." + print(note_msg) + notes.append(note_msg) + else: + error_msg = f"Error: '{key}' is not a valid object in the HDF5 file." + print(error_msg) + errors.append(error_msg) + + return len(errors) == 0, errors, notes
+ + + +
+[docs] +def update_hdf5_file_with_review(input_hdf5_file, review_yaml_file): + + """ + Updates, appends, or deletes metadata attributes in an HDF5 file based on a provided YAML dictionary. + + Parameters: + ----------- + input_hdf5_file : str + Path to the HDF5 file. + + yaml_dict : dict + Dictionary specifying objects and their attributes with operations. Example format: + { + "object_name": { "attributes" : "attr_name": { "value": attr_value, + "delete": true | false + } + } + } + """ + yaml_dict = load_yaml(review_yaml_file) + + success, errors, notes = validate_yaml_dict(input_hdf5_file,yaml_dict) + if not success: + raise ValueError(f"Review yaml file {review_yaml_file} is invalid. Validation errors: {errors}") + + # Initialize HDF5 operations manager + DataOpsAPI = hdf5_ops.HDF5DataOpsManager(input_hdf5_file) + DataOpsAPI.load_file_obj() + + # Iterate over each object in the YAML dictionary + for obj_name, attr_dict in yaml_dict.items(): + # Prepare dictionaries for append, update, and delete actions + append_dict = {} + update_dict = {} + delete_dict = {} + + if not obj_name in DataOpsAPI.file_obj: + continue # Skip if the object does not exist + + # Iterate over each attribute in the current object + for attr_name, attr_props in attr_dict['attributes'].items(): + if not isinstance(attr_props, dict): + #attr_props = {'value': attr_props} + # Check if the attribute exists (for updating) + if attr_name in DataOpsAPI.file_obj[obj_name].attrs: + update_dict[attr_name] = attr_props + # Otherwise, it's a new attribute to append + else: + append_dict[attr_name] = attr_props + else: + # Check if the attribute is marked for deletion + if attr_props.get('delete', False): + delete_dict[attr_name] = attr_props + + # Perform a single pass for all three operations + if append_dict: + DataOpsAPI.append_metadata(obj_name, append_dict) + if update_dict: + DataOpsAPI.update_metadata(obj_name, update_dict) + if delete_dict: + DataOpsAPI.delete_metadata(obj_name, delete_dict) + + # Close hdf5 file + DataOpsAPI.unload_file_obj() + # Regenerate yaml snapshot of updated HDF5 file + output_yml_filename_path = hdf5_ops.serialize_metadata(input_hdf5_file) + print(f'{output_yml_filename_path} was successfully regenerated from the updated version of{input_hdf5_file}')
+ + +
+[docs] +def count(hdf5_obj,yml_dict): + print(hdf5_obj.name) + if isinstance(hdf5_obj,h5py.Group) and len(hdf5_obj.name.split('/')) <= 4: + obj_review = yml_dict[hdf5_obj.name] + additions = [not (item in hdf5_obj.attrs.keys()) for item in obj_review['attributes'].keys()] + count_additions = sum(additions) + deletions = [not (item in obj_review['attributes'].keys()) for item in hdf5_obj.attrs.keys()] + count_delections = sum(deletions) + print('additions',count_additions, 'deletions', count_delections)
+ + +if __name__ == "__main__": + + if len(sys.argv) < 4: + print("Usage: python metadata_revision.py update <path/to/target_file.hdf5> <path/to/metadata_review_file.yaml>") + sys.exit(1) + + + if sys.argv[1] == 'update': + input_hdf5_file = sys.argv[2] + review_yaml_file = sys.argv[3] + update_hdf5_file_with_review(input_hdf5_file, review_yaml_file) + #run(sys.argv[2]) +
+ +
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/docs/build/html/_modules/src/data_integration_lib.html b/docs/build/html/_modules/src/data_integration_lib.html index 4901db9..31779e8 100644 --- a/docs/build/html/_modules/src/data_integration_lib.html +++ b/docs/build/html/_modules/src/data_integration_lib.html @@ -1,210 +1,210 @@ - - - - - - src.data_integration_lib — DIMA 1.0.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for src.data_integration_lib

-import os
-
-import src.hdf5_lib as hdf5_lib
-import src.g5505_utils as utils
-import yaml
- 
-import logging
-from datetime import datetime
-
-
-
-
-[docs] -def integrate_data_sources(yaml_config_file_path, log_dir='logs/'): - - """ Integrates data sources specified by the input configuration file into HDF5 files. - - Parameters: - yaml_config_file_path (str): Path to the YAML configuration file. - log_dir (str): Directory to save the log file. - - Returns: - str: Path (or list of Paths) to the created HDF5 file(s). - """ - - date = utils.created_at() - utils.setup_logging(log_dir, f"integrate_data_sources_{date}.log") - - with open(yaml_config_file_path,'r') as stream: - try: - config_dict = yaml.load(stream, Loader=yaml.FullLoader) - except yaml.YAMLError as exc: - logging.error("Error loading YAML file: %s", exc) - raise - - def output_filename(name, date, initials): - return f"{name}_{date}_{initials}.h5" - - exp_campaign_name = config_dict['experiment'] - initials = config_dict['contact'] - input_file_dir = config_dict['input_file_directory'] - output_dir = config_dict['output_file_directory'] - select_dir_keywords = config_dict['instrument_datafolder'] - root_metadata_dict = { - 'project' : config_dict['project'], - 'experiment' : config_dict['experiment'], - 'contact' : config_dict['contact'], - 'actris_level': config_dict['actris_level'] - } - - def create_hdf5_file(date_str, select_file_keywords,root_metadata): - filename = output_filename(exp_campaign_name, date_str, initials) - output_path = os.path.join(output_dir, filename) - logging.info("Creating HDF5 file at: %s", output_path) - - return hdf5_lib.create_hdf5_file_from_filesystem_path( - output_path, input_file_dir, select_dir_keywords, select_file_keywords, root_metadata_dict=root_metadata - ) - - if config_dict.get('datetime_steps'): - - datetime_augment_dict = {} - for datetime_step in config_dict['datetime_steps']: - tmp = datetime.strptime(datetime_step,'%Y-%m-%d %H-%M-%S') #convert(datetime_step) - datetime_augment_dict[tmp] = [tmp.strftime('%Y-%m-%d'),tmp.strftime('%Y_%m_%d'),tmp.strftime('%Y.%m.%d'),tmp.strftime('%Y%m%d')] - print(tmp) - - if 'single_experiment' in config_dict['integration_mode']: - output_filename_path = [] - for datetime_step in datetime_augment_dict.keys(): - date_str = datetime_step.strftime('%Y-%m-%d') - select_file_keywords = datetime_augment_dict[datetime_step] - - root_metadata_dict.update({'dataset_startdate': date_str, - 'dataset_enddate': date_str}) - dt_step_output_filename_path= create_hdf5_file(date_str, select_file_keywords, root_metadata_dict) - output_filename_path.append(dt_step_output_filename_path) - - elif 'collection' in config_dict['integration_mode']: - select_file_keywords = [] - for datetime_step in datetime_augment_dict.keys(): - select_file_keywords = select_file_keywords + datetime_augment_dict[datetime_step] - - config_dict['dataset_startdate'] = min(datetime_augment_dict.keys()) - config_dict['dataset_enddate'] = max(datetime_augment_dict.keys()) - startdate = config_dict['dataset_startdate'].strftime('%Y-%m-%d') - enddate = config_dict['dataset_enddate'].strftime('%Y-%m-%d') - root_metadata_dict.update({'dataset_startdate': startdate, - 'dataset_enddate': enddate}) - - date_str = f'{startdate}_{enddate}' - output_filename_path = create_hdf5_file(date_str, select_file_keywords, root_metadata_dict) - else: - startdate = config_dict['dataset_startdate'] - enddate = config_dict['dataset_enddate'] - root_metadata_dict.update({'dataset_startdate': startdate, - 'dataset_enddate': enddate}) - date_str = f'{startdate}_{enddate}' - output_filename_path = create_hdf5_file(date_str, select_file_keywords = [], root_metadata = root_metadata_dict) - - return output_filename_path
- -
- -
-
- -
-
-
-
- - - + + + + + + src.data_integration_lib — DIMA 1.0.0 documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for src.data_integration_lib

+import os
+
+import src.hdf5_lib as hdf5_lib
+import src.g5505_utils as utils
+import yaml
+ 
+import logging
+from datetime import datetime
+
+
+
+
+[docs] +def integrate_data_sources(yaml_config_file_path, log_dir='logs/'): + + """ Integrates data sources specified by the input configuration file into HDF5 files. + + Parameters: + yaml_config_file_path (str): Path to the YAML configuration file. + log_dir (str): Directory to save the log file. + + Returns: + str: Path (or list of Paths) to the created HDF5 file(s). + """ + + date = utils.created_at() + utils.setup_logging(log_dir, f"integrate_data_sources_{date}.log") + + with open(yaml_config_file_path,'r') as stream: + try: + config_dict = yaml.load(stream, Loader=yaml.FullLoader) + except yaml.YAMLError as exc: + logging.error("Error loading YAML file: %s", exc) + raise + + def output_filename(name, date, initials): + return f"{name}_{date}_{initials}.h5" + + exp_campaign_name = config_dict['experiment'] + initials = config_dict['contact'] + input_file_dir = config_dict['input_file_directory'] + output_dir = config_dict['output_file_directory'] + select_dir_keywords = config_dict['instrument_datafolder'] + root_metadata_dict = { + 'project' : config_dict['project'], + 'experiment' : config_dict['experiment'], + 'contact' : config_dict['contact'], + 'actris_level': config_dict['actris_level'] + } + + def create_hdf5_file(date_str, select_file_keywords,root_metadata): + filename = output_filename(exp_campaign_name, date_str, initials) + output_path = os.path.join(output_dir, filename) + logging.info("Creating HDF5 file at: %s", output_path) + + return hdf5_lib.create_hdf5_file_from_filesystem_path( + output_path, input_file_dir, select_dir_keywords, select_file_keywords, root_metadata_dict=root_metadata + ) + + if config_dict.get('datetime_steps'): + + datetime_augment_dict = {} + for datetime_step in config_dict['datetime_steps']: + tmp = datetime.strptime(datetime_step,'%Y-%m-%d %H-%M-%S') #convert(datetime_step) + datetime_augment_dict[tmp] = [tmp.strftime('%Y-%m-%d'),tmp.strftime('%Y_%m_%d'),tmp.strftime('%Y.%m.%d'),tmp.strftime('%Y%m%d')] + print(tmp) + + if 'single_experiment' in config_dict['integration_mode']: + output_filename_path = [] + for datetime_step in datetime_augment_dict.keys(): + date_str = datetime_step.strftime('%Y-%m-%d') + select_file_keywords = datetime_augment_dict[datetime_step] + + root_metadata_dict.update({'dataset_startdate': date_str, + 'dataset_enddate': date_str}) + dt_step_output_filename_path= create_hdf5_file(date_str, select_file_keywords, root_metadata_dict) + output_filename_path.append(dt_step_output_filename_path) + + elif 'collection' in config_dict['integration_mode']: + select_file_keywords = [] + for datetime_step in datetime_augment_dict.keys(): + select_file_keywords = select_file_keywords + datetime_augment_dict[datetime_step] + + config_dict['dataset_startdate'] = min(datetime_augment_dict.keys()) + config_dict['dataset_enddate'] = max(datetime_augment_dict.keys()) + startdate = config_dict['dataset_startdate'].strftime('%Y-%m-%d') + enddate = config_dict['dataset_enddate'].strftime('%Y-%m-%d') + root_metadata_dict.update({'dataset_startdate': startdate, + 'dataset_enddate': enddate}) + + date_str = f'{startdate}_{enddate}' + output_filename_path = create_hdf5_file(date_str, select_file_keywords, root_metadata_dict) + else: + startdate = config_dict['dataset_startdate'] + enddate = config_dict['dataset_enddate'] + root_metadata_dict.update({'dataset_startdate': startdate, + 'dataset_enddate': enddate}) + date_str = f'{startdate}_{enddate}' + output_filename_path = create_hdf5_file(date_str, select_file_keywords = [], root_metadata = root_metadata_dict) + + return output_filename_path
+ +
+ +
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/docs/build/html/_modules/src/g5505_file_reader.html b/docs/build/html/_modules/src/g5505_file_reader.html index 0342214..a01d189 100644 --- a/docs/build/html/_modules/src/g5505_file_reader.html +++ b/docs/build/html/_modules/src/g5505_file_reader.html @@ -1,448 +1,448 @@ - - - - - - src.g5505_file_reader — DIMA 1.0.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for src.g5505_file_reader

-import os
-
-import numpy as np
-import pandas as pd
-import collections
-from igor2.binarywave import load as loadibw
-
-import src.g5505_utils as utils
-#import src.metadata_review_lib as metadata
-#from src.metadata_review_lib import parse_attribute
-
-import yaml
-import h5py
-
-ROOT_DIR = os.path.abspath(os.curdir)
-
-
-[docs] -def read_xps_ibw_file_as_dict(filename): - """ - Reads IBW files from the Multiphase Chemistry Group, which contain XPS spectra and acquisition settings, - and formats the data into a dictionary with the structure {datasets: list of datasets}. Each dataset in the - list has the following structure: - - { - 'name': 'name', - 'data': data_array, - 'data_units': 'units', - 'shape': data_shape, - 'dtype': data_type - } - - Parameters - ---------- - filename : str - The IBW filename from the Multiphase Chemistry Group beamline. - - Returns - ------- - file_dict : dict - A dictionary containing the datasets from the IBW file. - - Raises - ------ - ValueError - If the input IBW file is not a valid IBW file. - - """ - - - file_obj = loadibw(filename) - - required_keys = ['wData','data_units','dimension_units','note'] - if sum([item in required_keys for item in file_obj['wave'].keys()]) < len(required_keys): - raise ValueError('This is not a valid xps ibw file. It does not satisfy minimum adimissibility criteria.') - - file_dict = {} - path_tail, path_head = os.path.split(filename) - - # Group name and attributes - file_dict['name'] = path_head - file_dict['attributes_dict'] = {} - - # Convert notes of bytes class to string class and split string into a list of elements separated by '\r'. - notes_list = file_obj['wave']['note'].decode("utf-8").split('\r') - exclude_list = ['Excitation Energy'] - for item in notes_list: - if '=' in item: - key, value = tuple(item.split('=')) - # TODO: check if value can be converted into a numeric type. Now all values are string type - if not key in exclude_list: - file_dict['attributes_dict'][key] = value - - # TODO: talk to Thorsten to see if there is an easier way to access the below attributes - dimension_labels = file_obj['wave']['dimension_units'].decode("utf-8").split(']') - file_dict['attributes_dict']['dimension_units'] = [item+']' for item in dimension_labels[0:len(dimension_labels)-1]] - - # Datasets and their attributes - - file_dict['datasets'] = [] - - dataset = {} - dataset['name'] = 'spectrum' - dataset['data'] = file_obj['wave']['wData'] - dataset['data_units'] = file_obj['wave']['data_units'] - dataset['shape'] = dataset['data'].shape - dataset['dtype'] = type(dataset['data']) - - # TODO: include energy axis dataset - - file_dict['datasets'].append(dataset) - - - return file_dict
- - -
-[docs] -def copy_file_in_group(source_file_path, dest_file_obj : h5py.File, dest_group_name, work_with_copy : bool = True): - # Create copy of original file to avoid possible file corruption and work with it. - - if work_with_copy: - tmp_file_path = utils.make_file_copy(source_file_path) - else: - tmp_file_path = source_file_path - - # Open backup h5 file and copy complet filesystem directory onto a group in h5file - with h5py.File(tmp_file_path,'r') as src_file: - dest_file_obj.copy(source= src_file['/'], dest= dest_group_name) - - if 'tmp_files' in tmp_file_path: - os.remove(tmp_file_path)
- - -
-[docs] -def read_txt_files_as_dict(filename : str , work_with_copy : bool = True ): - - # Get the directory of the current module - module_dir = os.path.dirname(__file__) - # Construct the relative file path - instrument_configs_path = os.path.join(module_dir, 'instruments', 'text_data_sources.yaml') - - with open(instrument_configs_path,'r') as stream: - try: - config_dict = yaml.load(stream, Loader=yaml.FullLoader) - except yaml.YAMLError as exc: - print(exc) - # Verify if file can be read by available intrument configurations. - if not any(key in filename.replace(os.sep,'/') for key in config_dict.keys()): - return {} - - - #TODO: this may be prone to error if assumed folder structure is non compliant - file_encoding = config_dict['default']['file_encoding'] #'utf-8' - separator = config_dict['default']['separator'] - table_header = config_dict['default']['table_header'] - - for key in config_dict.keys(): - if key.replace('/',os.sep) in filename: - file_encoding = config_dict[key].get('file_encoding',file_encoding) - separator = config_dict[key].get('separator',separator).replace('\\t','\t') - table_header = config_dict[key].get('table_header',table_header) - timestamp_variables = config_dict[key].get('timestamp',[]) - datetime_format = config_dict[key].get('datetime_format',[]) - - description_dict = {} - #link_to_description = config_dict[key].get('link_to_description',[]).replace('/',os.sep) - link_to_description = os.path.join(module_dir,config_dict[key].get('link_to_description',[]).replace('/',os.sep)) - with open(link_to_description,'r') as stream: - try: - description_dict = yaml.load(stream, Loader=yaml.FullLoader) - except yaml.YAMLError as exc: - print(exc) - break - #if 'None' in table_header: - # return {} - - # Read header as a dictionary and detect where data table starts - header_dict = {} - data_start = False - # Work with copy of the file for safety - if work_with_copy: - tmp_filename = utils.make_file_copy(source_file_path=filename) - else: - tmp_filename = filename - - #with open(tmp_filename,'rb',encoding=file_encoding,errors='ignore') as f: - with open(tmp_filename,'rb') as f: - table_preamble = [] - for line_number, line in enumerate(f): - - if table_header in line.decode(file_encoding): - list_of_substrings = line.decode(file_encoding).split(separator) - - # Count occurrences of each substring - substring_counts = collections.Counter(list_of_substrings) - data_start = True - # Generate column names with appended index only for repeated substrings - column_names = [f"{i}_{name.strip()}" if substring_counts[name] > 1 else name.strip() for i, name in enumerate(list_of_substrings)] - - #column_names = [str(i)+'_'+name.strip() for i, name in enumerate(list_of_substrings)] - #column_names = [] - #for i, name in enumerate(list_of_substrings): - # column_names.append(str(i)+'_'+name) - - #print(line_number, len(column_names ),'\n') - break - # Subdivide line into words, and join them by single space. - # I asumme this can produce a cleaner line that contains no weird separator characters \t \r or extra spaces and so on. - list_of_substrings = line.decode(file_encoding).split() - # TODO: ideally we should use a multilinear string but the yalm parser is not recognizing \n as special character - #line = ' '.join(list_of_substrings+['\n']) - #line = ' '.join(list_of_substrings) - table_preamble.append(' '.join([item for item in list_of_substrings]))# += new_line - - # Represent string values as fixed length strings in the HDF5 file, which need - # to be decoded as string when we read them. It provides better control than variable strings, - # at the expense of flexibility. - # https://docs.h5py.org/en/stable/strings.html - - if table_preamble: - header_dict["table_preamble"] = utils.convert_string_to_bytes(table_preamble) - - - - # TODO: it does not work with separator as none :(. fix for RGA - try: - df = pd.read_csv(tmp_filename, - delimiter = separator, - header=line_number, - #encoding='latin-1', - encoding = file_encoding, - names=column_names, - skip_blank_lines=True) - - df_numerical_attrs = df.select_dtypes(include ='number') - df_categorical_attrs = df.select_dtypes(exclude='number') - numerical_variables = [item for item in df_numerical_attrs.columns] - - # Consolidate into single timestamp column the separate columns 'date' 'time' specified in text_data_source.yaml - if timestamp_variables: - #df_categorical_attrs['timestamps'] = [' '.join(df_categorical_attrs.loc[i,timestamp_variables].to_numpy()) for i in df.index] - #df_categorical_attrs['timestamps'] = [ df_categorical_attrs.loc[i,'0_Date']+' '+df_categorical_attrs.loc[i,'1_Time'] for i in df.index] - - - #df_categorical_attrs['timestamps'] = df_categorical_attrs[timestamp_variables].astype(str).agg(' '.join, axis=1) - timestamps_name = ' '.join(timestamp_variables) - df_categorical_attrs[ timestamps_name] = df_categorical_attrs[timestamp_variables].astype(str).agg(' '.join, axis=1) - - valid_indices = [] - if datetime_format: - df_categorical_attrs[ timestamps_name] = pd.to_datetime(df_categorical_attrs[ timestamps_name],format=datetime_format,errors='coerce') - valid_indices = df_categorical_attrs.dropna(subset=[timestamps_name]).index - df_categorical_attrs = df_categorical_attrs.loc[valid_indices,:] - df_numerical_attrs = df_numerical_attrs.loc[valid_indices,:] - - df_categorical_attrs[timestamps_name] = df_categorical_attrs[timestamps_name].dt.strftime(config_dict['default']['desired_format']) - startdate = df_categorical_attrs[timestamps_name].min() - enddate = df_categorical_attrs[timestamps_name].max() - - df_categorical_attrs[timestamps_name] = df_categorical_attrs[timestamps_name].astype(str) - #header_dict.update({'stastrrtdate':startdate,'enddate':enddate}) - header_dict['startdate']= str(startdate) - header_dict['enddate']=str(enddate) - - if len(timestamp_variables) > 1: - df_categorical_attrs = df_categorical_attrs.drop(columns = timestamp_variables) - - - #df_categorical_attrs.reindex(drop=True) - #df_numerical_attrs.reindex(drop=True) - - - - categorical_variables = [item for item in df_categorical_attrs.columns] - #### - #elif 'RGA' in filename: - # df_categorical_attrs = df_categorical_attrs.rename(columns={'0_Time(s)' : 'timestamps'}) - - ### - file_dict = {} - path_tail, path_head = os.path.split(tmp_filename) - - file_dict['name'] = path_head - # TODO: review this header dictionary, it may not be the best way to represent header data - file_dict['attributes_dict'] = header_dict - file_dict['datasets'] = [] - #### - - df = pd.concat((df_categorical_attrs,df_numerical_attrs),axis=1) - - #if numerical_variables: - dataset = {} - dataset['name'] = 'data_table'#_numerical_variables' - dataset['data'] = utils.dataframe_to_np_structured_array(df) #df_numerical_attrs.to_numpy() - dataset['shape'] = dataset['data'].shape - dataset['dtype'] = type(dataset['data']) - #dataset['data_units'] = file_obj['wave']['data_units'] - # - # Create attribute descriptions based on description_dict - dataset['attributes'] = {} - - for column_name in df.columns: - column_attr_dict = description_dict['table_header'].get(column_name, - {'note':'there was no description available. Review instrument files.'}) - dataset['attributes'].update({column_name: utils.parse_attribute(column_attr_dict)}) - - #try: - # dataset['attributes'] = description_dict['table_header'].copy() - # for key in description_dict['table_header'].keys(): - # if not key in numerical_variables: - # dataset['attributes'].pop(key) # delete key - # else: - # dataset['attributes'][key] = utils.parse_attribute(dataset['attributes'][key]) - # if timestamps_name in categorical_variables: - # dataset['attributes'][timestamps_name] = utils.parse_attribute({'unit':'YYYY-MM-DD HH:MM:SS.ffffff'}) - #except ValueError as err: - # print(err) - - file_dict['datasets'].append(dataset) - - - #if categorical_variables: - # dataset = {} - # dataset['name'] = 'table_categorical_variables' - # dataset['data'] = dataframe_to_np_structured_array(df_categorical_attrs) #df_categorical_attrs.loc[:,categorical_variables].to_numpy() - # dataset['shape'] = dataset['data'].shape - # dataset['dtype'] = type(dataset['data']) - # if timestamps_name in categorical_variables: - # dataset['attributes'] = {timestamps_name: utils.parse_attribute({'unit':'YYYY-MM-DD HH:MM:SS.ffffff'})} - # file_dict['datasets'].append(dataset) - - - - - except: - return {} - - return file_dict
- - -
-[docs] -def main(): - - inputfile_dir = '\\\\fs101\\5505\\People\\Juan\\TypicalBeamTime' - - file_dict = read_xps_ibw_file_as_dict(inputfile_dir+'\\SES\\0069069_N1s_495eV.ibw') - - for key in file_dict.keys(): - print(key,file_dict[key])
- - - -if __name__ == '__main__': - - main() - - print(':)') -
- -
-
- -
-
-
-
- - - + + + + + + src.g5505_file_reader — DIMA 1.0.0 documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for src.g5505_file_reader

+import os
+
+import numpy as np
+import pandas as pd
+import collections
+from igor2.binarywave import load as loadibw
+
+import src.g5505_utils as utils
+#import src.metadata_review_lib as metadata
+#from src.metadata_review_lib import parse_attribute
+
+import yaml
+import h5py
+
+ROOT_DIR = os.path.abspath(os.curdir)
+
+
+[docs] +def read_xps_ibw_file_as_dict(filename): + """ + Reads IBW files from the Multiphase Chemistry Group, which contain XPS spectra and acquisition settings, + and formats the data into a dictionary with the structure {datasets: list of datasets}. Each dataset in the + list has the following structure: + + { + 'name': 'name', + 'data': data_array, + 'data_units': 'units', + 'shape': data_shape, + 'dtype': data_type + } + + Parameters + ---------- + filename : str + The IBW filename from the Multiphase Chemistry Group beamline. + + Returns + ------- + file_dict : dict + A dictionary containing the datasets from the IBW file. + + Raises + ------ + ValueError + If the input IBW file is not a valid IBW file. + + """ + + + file_obj = loadibw(filename) + + required_keys = ['wData','data_units','dimension_units','note'] + if sum([item in required_keys for item in file_obj['wave'].keys()]) < len(required_keys): + raise ValueError('This is not a valid xps ibw file. It does not satisfy minimum adimissibility criteria.') + + file_dict = {} + path_tail, path_head = os.path.split(filename) + + # Group name and attributes + file_dict['name'] = path_head + file_dict['attributes_dict'] = {} + + # Convert notes of bytes class to string class and split string into a list of elements separated by '\r'. + notes_list = file_obj['wave']['note'].decode("utf-8").split('\r') + exclude_list = ['Excitation Energy'] + for item in notes_list: + if '=' in item: + key, value = tuple(item.split('=')) + # TODO: check if value can be converted into a numeric type. Now all values are string type + if not key in exclude_list: + file_dict['attributes_dict'][key] = value + + # TODO: talk to Thorsten to see if there is an easier way to access the below attributes + dimension_labels = file_obj['wave']['dimension_units'].decode("utf-8").split(']') + file_dict['attributes_dict']['dimension_units'] = [item+']' for item in dimension_labels[0:len(dimension_labels)-1]] + + # Datasets and their attributes + + file_dict['datasets'] = [] + + dataset = {} + dataset['name'] = 'spectrum' + dataset['data'] = file_obj['wave']['wData'] + dataset['data_units'] = file_obj['wave']['data_units'] + dataset['shape'] = dataset['data'].shape + dataset['dtype'] = type(dataset['data']) + + # TODO: include energy axis dataset + + file_dict['datasets'].append(dataset) + + + return file_dict
+ + +
+[docs] +def copy_file_in_group(source_file_path, dest_file_obj : h5py.File, dest_group_name, work_with_copy : bool = True): + # Create copy of original file to avoid possible file corruption and work with it. + + if work_with_copy: + tmp_file_path = utils.make_file_copy(source_file_path) + else: + tmp_file_path = source_file_path + + # Open backup h5 file and copy complet filesystem directory onto a group in h5file + with h5py.File(tmp_file_path,'r') as src_file: + dest_file_obj.copy(source= src_file['/'], dest= dest_group_name) + + if 'tmp_files' in tmp_file_path: + os.remove(tmp_file_path)
+ + +
+[docs] +def read_txt_files_as_dict(filename : str , work_with_copy : bool = True ): + + # Get the directory of the current module + module_dir = os.path.dirname(__file__) + # Construct the relative file path + instrument_configs_path = os.path.join(module_dir, 'instruments', 'text_data_sources.yaml') + + with open(instrument_configs_path,'r') as stream: + try: + config_dict = yaml.load(stream, Loader=yaml.FullLoader) + except yaml.YAMLError as exc: + print(exc) + # Verify if file can be read by available intrument configurations. + if not any(key in filename.replace(os.sep,'/') for key in config_dict.keys()): + return {} + + + #TODO: this may be prone to error if assumed folder structure is non compliant + file_encoding = config_dict['default']['file_encoding'] #'utf-8' + separator = config_dict['default']['separator'] + table_header = config_dict['default']['table_header'] + + for key in config_dict.keys(): + if key.replace('/',os.sep) in filename: + file_encoding = config_dict[key].get('file_encoding',file_encoding) + separator = config_dict[key].get('separator',separator).replace('\\t','\t') + table_header = config_dict[key].get('table_header',table_header) + timestamp_variables = config_dict[key].get('timestamp',[]) + datetime_format = config_dict[key].get('datetime_format',[]) + + description_dict = {} + #link_to_description = config_dict[key].get('link_to_description',[]).replace('/',os.sep) + link_to_description = os.path.join(module_dir,config_dict[key].get('link_to_description',[]).replace('/',os.sep)) + with open(link_to_description,'r') as stream: + try: + description_dict = yaml.load(stream, Loader=yaml.FullLoader) + except yaml.YAMLError as exc: + print(exc) + break + #if 'None' in table_header: + # return {} + + # Read header as a dictionary and detect where data table starts + header_dict = {} + data_start = False + # Work with copy of the file for safety + if work_with_copy: + tmp_filename = utils.make_file_copy(source_file_path=filename) + else: + tmp_filename = filename + + #with open(tmp_filename,'rb',encoding=file_encoding,errors='ignore') as f: + with open(tmp_filename,'rb') as f: + table_preamble = [] + for line_number, line in enumerate(f): + + if table_header in line.decode(file_encoding): + list_of_substrings = line.decode(file_encoding).split(separator) + + # Count occurrences of each substring + substring_counts = collections.Counter(list_of_substrings) + data_start = True + # Generate column names with appended index only for repeated substrings + column_names = [f"{i}_{name.strip()}" if substring_counts[name] > 1 else name.strip() for i, name in enumerate(list_of_substrings)] + + #column_names = [str(i)+'_'+name.strip() for i, name in enumerate(list_of_substrings)] + #column_names = [] + #for i, name in enumerate(list_of_substrings): + # column_names.append(str(i)+'_'+name) + + #print(line_number, len(column_names ),'\n') + break + # Subdivide line into words, and join them by single space. + # I asumme this can produce a cleaner line that contains no weird separator characters \t \r or extra spaces and so on. + list_of_substrings = line.decode(file_encoding).split() + # TODO: ideally we should use a multilinear string but the yalm parser is not recognizing \n as special character + #line = ' '.join(list_of_substrings+['\n']) + #line = ' '.join(list_of_substrings) + table_preamble.append(' '.join([item for item in list_of_substrings]))# += new_line + + # Represent string values as fixed length strings in the HDF5 file, which need + # to be decoded as string when we read them. It provides better control than variable strings, + # at the expense of flexibility. + # https://docs.h5py.org/en/stable/strings.html + + if table_preamble: + header_dict["table_preamble"] = utils.convert_string_to_bytes(table_preamble) + + + + # TODO: it does not work with separator as none :(. fix for RGA + try: + df = pd.read_csv(tmp_filename, + delimiter = separator, + header=line_number, + #encoding='latin-1', + encoding = file_encoding, + names=column_names, + skip_blank_lines=True) + + df_numerical_attrs = df.select_dtypes(include ='number') + df_categorical_attrs = df.select_dtypes(exclude='number') + numerical_variables = [item for item in df_numerical_attrs.columns] + + # Consolidate into single timestamp column the separate columns 'date' 'time' specified in text_data_source.yaml + if timestamp_variables: + #df_categorical_attrs['timestamps'] = [' '.join(df_categorical_attrs.loc[i,timestamp_variables].to_numpy()) for i in df.index] + #df_categorical_attrs['timestamps'] = [ df_categorical_attrs.loc[i,'0_Date']+' '+df_categorical_attrs.loc[i,'1_Time'] for i in df.index] + + + #df_categorical_attrs['timestamps'] = df_categorical_attrs[timestamp_variables].astype(str).agg(' '.join, axis=1) + timestamps_name = ' '.join(timestamp_variables) + df_categorical_attrs[ timestamps_name] = df_categorical_attrs[timestamp_variables].astype(str).agg(' '.join, axis=1) + + valid_indices = [] + if datetime_format: + df_categorical_attrs[ timestamps_name] = pd.to_datetime(df_categorical_attrs[ timestamps_name],format=datetime_format,errors='coerce') + valid_indices = df_categorical_attrs.dropna(subset=[timestamps_name]).index + df_categorical_attrs = df_categorical_attrs.loc[valid_indices,:] + df_numerical_attrs = df_numerical_attrs.loc[valid_indices,:] + + df_categorical_attrs[timestamps_name] = df_categorical_attrs[timestamps_name].dt.strftime(config_dict['default']['desired_format']) + startdate = df_categorical_attrs[timestamps_name].min() + enddate = df_categorical_attrs[timestamps_name].max() + + df_categorical_attrs[timestamps_name] = df_categorical_attrs[timestamps_name].astype(str) + #header_dict.update({'stastrrtdate':startdate,'enddate':enddate}) + header_dict['startdate']= str(startdate) + header_dict['enddate']=str(enddate) + + if len(timestamp_variables) > 1: + df_categorical_attrs = df_categorical_attrs.drop(columns = timestamp_variables) + + + #df_categorical_attrs.reindex(drop=True) + #df_numerical_attrs.reindex(drop=True) + + + + categorical_variables = [item for item in df_categorical_attrs.columns] + #### + #elif 'RGA' in filename: + # df_categorical_attrs = df_categorical_attrs.rename(columns={'0_Time(s)' : 'timestamps'}) + + ### + file_dict = {} + path_tail, path_head = os.path.split(tmp_filename) + + file_dict['name'] = path_head + # TODO: review this header dictionary, it may not be the best way to represent header data + file_dict['attributes_dict'] = header_dict + file_dict['datasets'] = [] + #### + + df = pd.concat((df_categorical_attrs,df_numerical_attrs),axis=1) + + #if numerical_variables: + dataset = {} + dataset['name'] = 'data_table'#_numerical_variables' + dataset['data'] = utils.dataframe_to_np_structured_array(df) #df_numerical_attrs.to_numpy() + dataset['shape'] = dataset['data'].shape + dataset['dtype'] = type(dataset['data']) + #dataset['data_units'] = file_obj['wave']['data_units'] + # + # Create attribute descriptions based on description_dict + dataset['attributes'] = {} + + for column_name in df.columns: + column_attr_dict = description_dict['table_header'].get(column_name, + {'note':'there was no description available. Review instrument files.'}) + dataset['attributes'].update({column_name: utils.parse_attribute(column_attr_dict)}) + + #try: + # dataset['attributes'] = description_dict['table_header'].copy() + # for key in description_dict['table_header'].keys(): + # if not key in numerical_variables: + # dataset['attributes'].pop(key) # delete key + # else: + # dataset['attributes'][key] = utils.parse_attribute(dataset['attributes'][key]) + # if timestamps_name in categorical_variables: + # dataset['attributes'][timestamps_name] = utils.parse_attribute({'unit':'YYYY-MM-DD HH:MM:SS.ffffff'}) + #except ValueError as err: + # print(err) + + file_dict['datasets'].append(dataset) + + + #if categorical_variables: + # dataset = {} + # dataset['name'] = 'table_categorical_variables' + # dataset['data'] = dataframe_to_np_structured_array(df_categorical_attrs) #df_categorical_attrs.loc[:,categorical_variables].to_numpy() + # dataset['shape'] = dataset['data'].shape + # dataset['dtype'] = type(dataset['data']) + # if timestamps_name in categorical_variables: + # dataset['attributes'] = {timestamps_name: utils.parse_attribute({'unit':'YYYY-MM-DD HH:MM:SS.ffffff'})} + # file_dict['datasets'].append(dataset) + + + + + except: + return {} + + return file_dict
+ + +
+[docs] +def main(): + + inputfile_dir = '\\\\fs101\\5505\\People\\Juan\\TypicalBeamTime' + + file_dict = read_xps_ibw_file_as_dict(inputfile_dir+'\\SES\\0069069_N1s_495eV.ibw') + + for key in file_dict.keys(): + print(key,file_dict[key])
+ + + +if __name__ == '__main__': + + main() + + print(':)') +
+ +
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/docs/build/html/_modules/src/hdf5_data_extraction.html b/docs/build/html/_modules/src/hdf5_data_extraction.html index 5f0ecc3..a1c9e0a 100644 --- a/docs/build/html/_modules/src/hdf5_data_extraction.html +++ b/docs/build/html/_modules/src/hdf5_data_extraction.html @@ -1,164 +1,164 @@ - - - - - - src.hdf5_data_extraction — DIMA 1.0.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for src.hdf5_data_extraction

-import h5py
-import pandas as pd
-import numpy as np
-import os
-import src.hdf5_vis as hdf5_vis
-
-
-
-[docs] -def read_dataset_from_hdf5file(hdf5_file_path, dataset_path): - # Open the HDF5 file - with h5py.File(hdf5_file_path, 'r') as hdf: - # Load the dataset - dataset = hdf[dataset_path] - data = np.empty(dataset.shape, dtype=dataset.dtype) - dataset.read_direct(data) - df = pd.DataFrame(data) - - for col_name in df.select_dtypes(exclude='number'): - df[col_name] = df[col_name].str.decode('utf-8') #apply(lambda x: x.decode('utf-8') if isinstance(x,bytes) else x) - ## Extract metadata (attributes) and convert to a dictionary - #metadata = hdf5_vis.construct_attributes_dict(hdf[dataset_name].attrs) - ## Create a one-row DataFrame with the metadata - #metadata_df = pd.DataFrame.from_dict(data, orient='columns') - return df
- - -
-[docs] -def read_metadata_from_hdf5obj(hdf5_file_path, obj_path): - # TODO: Complete this function - metadata_df = pd.DataFrame.empty() - return metadata_df
- - -
-[docs] -def list_datasets_in_hdf5file(hdf5_file_path): - - def get_datasets(name, obj, list_of_datasets): - if isinstance(obj,h5py.Dataset): - list_of_datasets.append(name) - #print(f'Adding dataset: {name}') #tail: {head} head: {tail}') - - - with h5py.File(hdf5_file_path,'r') as file: - list_of_datasets = [] - file.visititems(lambda name, obj: get_datasets(name, obj, list_of_datasets)) - - dataset_df = pd.DataFrame({'dataset_name':list_of_datasets}) - - dataset_df['parent_instrument'] = dataset_df['dataset_name'].apply(lambda x: x.split('/')[-3]) - dataset_df['parent_file'] = dataset_df['dataset_name'].apply(lambda x: x.split('/')[-2]) - - return dataset_df
- -
- -
-
- -
-
-
-
- - - + + + + + + src.hdf5_data_extraction — DIMA 1.0.0 documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for src.hdf5_data_extraction

+import h5py
+import pandas as pd
+import numpy as np
+import os
+import src.hdf5_vis as hdf5_vis
+
+
+
+[docs] +def read_dataset_from_hdf5file(hdf5_file_path, dataset_path): + # Open the HDF5 file + with h5py.File(hdf5_file_path, 'r') as hdf: + # Load the dataset + dataset = hdf[dataset_path] + data = np.empty(dataset.shape, dtype=dataset.dtype) + dataset.read_direct(data) + df = pd.DataFrame(data) + + for col_name in df.select_dtypes(exclude='number'): + df[col_name] = df[col_name].str.decode('utf-8') #apply(lambda x: x.decode('utf-8') if isinstance(x,bytes) else x) + ## Extract metadata (attributes) and convert to a dictionary + #metadata = hdf5_vis.construct_attributes_dict(hdf[dataset_name].attrs) + ## Create a one-row DataFrame with the metadata + #metadata_df = pd.DataFrame.from_dict(data, orient='columns') + return df
+ + +
+[docs] +def read_metadata_from_hdf5obj(hdf5_file_path, obj_path): + # TODO: Complete this function + metadata_df = pd.DataFrame.empty() + return metadata_df
+ + +
+[docs] +def list_datasets_in_hdf5file(hdf5_file_path): + + def get_datasets(name, obj, list_of_datasets): + if isinstance(obj,h5py.Dataset): + list_of_datasets.append(name) + #print(f'Adding dataset: {name}') #tail: {head} head: {tail}') + + + with h5py.File(hdf5_file_path,'r') as file: + list_of_datasets = [] + file.visititems(lambda name, obj: get_datasets(name, obj, list_of_datasets)) + + dataset_df = pd.DataFrame({'dataset_name':list_of_datasets}) + + dataset_df['parent_instrument'] = dataset_df['dataset_name'].apply(lambda x: x.split('/')[-3]) + dataset_df['parent_file'] = dataset_df['dataset_name'].apply(lambda x: x.split('/')[-2]) + + return dataset_df
+ +
+ +
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/docs/build/html/_modules/src/hdf5_lib.html b/docs/build/html/_modules/src/hdf5_lib.html index e3e3c6e..2bfbc0e 100644 --- a/docs/build/html/_modules/src/hdf5_lib.html +++ b/docs/build/html/_modules/src/hdf5_lib.html @@ -1,412 +1,412 @@ - - - - - - src.hdf5_lib — DIMA 1.0.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for src.hdf5_lib

-import sys
-import os
-root_dir = os.path.abspath(os.curdir)
-sys.path.append(root_dir)
-
-import pandas as pd
-import numpy as np
-import h5py
-import logging
-
-import utils.g5505_utils as utils
-import instruments.readers.filereader_registry as filereader_registry
-
- 
-   
-def __transfer_file_dict_to_hdf5(h5file, group_name, file_dict):
-    """
-    Transfers data from a file_dict to an HDF5 file.
-
-    Parameters
-    ----------
-    h5file : h5py.File
-        HDF5 file object where the data will be written.
-    group_name : str
-        Name of the HDF5 group where data will be stored.
-    file_dict : dict
-        Dictionary containing file data to be transferred. Required structure:
-        {
-            'name': str,
-            'attributes_dict': dict,
-            'datasets': [
-                {
-                    'name': str,
-                    'data': array-like,
-                    'shape': tuple,
-                    'attributes': dict (optional)
-                },
-                ...
-            ]
-        }
-
-    Returns
-    -------
-    None
-    """
-
-    if not file_dict:
-        return
-
-    try:
-        # Create group and add their attributes
-        group = h5file[group_name].create_group(name=file_dict['name'])
-        # Add group attributes                                
-        group.attrs.update(file_dict['attributes_dict'])
-        
-        # Add datasets to the just created group
-        for dataset in file_dict['datasets']:
-            dataset_obj = group.create_dataset(
-                name=dataset['name'], 
-                data=dataset['data'],
-                shape=dataset['shape']
-            )
-            
-            # Add dataset's attributes                                
-            attributes = dataset.get('attributes', {})
-            dataset_obj.attrs.update(attributes)
-        group.attrs['last_update_date'] = utils.created_at().encode('utf-8')
-    except Exception as inst: 
-        print(inst) 
-        logging.error('Failed to transfer data into HDF5: %s', inst)
-
-def __copy_file_in_group(source_file_path, dest_file_obj : h5py.File, dest_group_name, work_with_copy : bool = True):
-    # Create copy of original file to avoid possible file corruption and work with it.
-
-    if work_with_copy:
-        tmp_file_path = utils.make_file_copy(source_file_path)
-    else:
-        tmp_file_path = source_file_path
-
-    # Open backup h5 file and copy complet filesystem directory onto a group in h5file
-    with h5py.File(tmp_file_path,'r') as src_file:
-        dest_file_obj.copy(source= src_file['/'], dest= dest_group_name)
-
-    if 'tmp_files' in tmp_file_path:
-        os.remove(tmp_file_path)
-
-
-[docs] -def create_hdf5_file_from_filesystem_path(path_to_input_directory: str, - path_to_filenames_dict: dict = None, - select_dir_keywords : list = [], - root_metadata_dict : dict = {}, mode = 'w'): - - """ - Creates an .h5 file with name "output_filename" that preserves the directory tree (or folder structure) - of a given filesystem path. - - The data integration capabilities are limited by our file reader, which can only access data from a list of - admissible file formats. These, however, can be extended. Directories are groups in the resulting HDF5 file. - Files are formatted as composite objects consisting of a group, file, and attributes. - - Parameters - ---------- - output_filename : str - Name of the output HDF5 file. - path_to_input_directory : str - Path to root directory, specified with forward slashes, e.g., path/to/root. - - path_to_filenames_dict : dict, optional - A pre-processed dictionary where keys are directory paths on the input directory's tree and values are lists of files. - If provided, 'input_file_system_path' is ignored. - - select_dir_keywords : list - List of string elements to consider or select only directory paths that contain - a word in 'select_dir_keywords'. When empty, all directory paths are considered - to be included in the HDF5 file group hierarchy. - root_metadata_dict : dict - Metadata to include at the root level of the HDF5 file. - - mode : str - 'w' create File, truncate if it exists, or 'r+' read/write, File must exists. By default, mode = "w". - - Returns - ------- - output_filename : str - Path to the created HDF5 file. - """ - - - if not mode in ['w','r+']: - raise ValueError(f'Parameter mode must take values in ["w","r+"]') - - if not '/' in path_to_input_directory: - raise ValueError('path_to_input_directory needs to be specified using forward slashes "/".' ) - - #path_to_output_directory = os.path.join(path_to_input_directory,'..') - path_to_input_directory = os.path.normpath(path_to_input_directory).rstrip(os.sep) - - - for i, keyword in enumerate(select_dir_keywords): - select_dir_keywords[i] = keyword.replace('/',os.sep) - - if not path_to_filenames_dict: - # On dry_run=True, returns path to files dictionary of the output directory without making a actual copy of the input directory. - # Therefore, there wont be a copying conflict by setting up input and output directories the same - path_to_filenames_dict = utils.copy_directory_with_contraints(input_dir_path=path_to_input_directory, - output_dir_path=path_to_input_directory, - dry_run=True) - # Set input_directory as copied input directory - root_dir = path_to_input_directory - path_to_output_file = path_to_input_directory.rstrip(os.path.sep) + '.h5' - - with h5py.File(path_to_output_file, mode=mode, track_order=True) as h5file: - - number_of_dirs = len(path_to_filenames_dict.keys()) - dir_number = 1 - for dirpath, filtered_filenames_list in path_to_filenames_dict.items(): - - start_message = f'Starting to transfer files in directory: {dirpath}' - end_message = f'\nCompleted transferring files in directory: {dirpath}' - # Print and log the start message - print(start_message) - logging.info(start_message) - - # Check if filtered_filenames_list is nonempty. TODO: This is perhaps redundant by design of path_to_filenames_dict. - if not filtered_filenames_list: - continue - - group_name = dirpath.replace(os.sep,'/') - group_name = group_name.replace(root_dir.replace(os.sep,'/') + '/', '/') - - # Flatten group name to one level - if select_dir_keywords: - offset = sum([len(i.split(os.sep)) if i in dirpath else 0 for i in select_dir_keywords]) - else: - offset = 1 - tmp_list = group_name.split('/') - if len(tmp_list) > offset+1: - group_name = '/'.join([tmp_list[i] for i in range(offset+1)]) - - # Group hierarchy is implicitly defined by the forward slashes - if not group_name in h5file.keys(): - h5file.create_group(group_name) - h5file[group_name].attrs['creation_date'] = utils.created_at().encode('utf-8') - #h5file[group_name].attrs.create(name='filtered_file_list',data=convert_string_to_bytes(filtered_filename_list)) - #h5file[group_name].attrs.create(name='file_list',data=convert_string_to_bytes(filenames_list)) - else: - print(group_name,' was already created.') - - for filenumber, filename in enumerate(filtered_filenames_list): - - #file_ext = os.path.splitext(filename)[1] - #try: - - # hdf5 path to filename group - dest_group_name = f'{group_name}/{filename}' - - if not 'h5' in filename: - #file_dict = config_file.select_file_readers(group_id)[file_ext](os.path.join(dirpath,filename)) - #file_dict = ext_to_reader_dict[file_ext](os.path.join(dirpath,filename)) - file_dict = filereader_registry.select_file_reader(dest_group_name)(os.path.join(dirpath,filename)) - - __transfer_file_dict_to_hdf5(h5file, group_name, file_dict) - - else: - source_file_path = os.path.join(dirpath,filename) - dest_file_obj = h5file - #group_name +'/'+filename - #ext_to_reader_dict[file_ext](source_file_path, dest_file_obj, dest_group_name) - #g5505f_reader.select_file_reader(dest_group_name)(source_file_path, dest_file_obj, dest_group_name) - __copy_file_in_group(source_file_path, dest_file_obj, dest_group_name, False) - - # Update the progress bar and log the end message - utils.progressBar(dir_number, number_of_dirs, end_message) - logging.info(end_message) - dir_number = dir_number + 1 - - - - if len(root_metadata_dict.keys())>0: - for key, value in root_metadata_dict.items(): - #if key in h5file.attrs: - # del h5file.attrs[key] - h5file.attrs.create(key, value) - #annotate_root_dir(output_filename,root_metadata_dict) - - - #output_yml_filename_path = hdf5_vis.take_yml_snapshot_of_hdf5_file(output_filename) - - return path_to_output_file #, output_yml_filename_path
- - -
-[docs] -def save_processed_dataframe_to_hdf5(df, annotator, output_filename): # src_hdf5_path, script_date, script_name): - """ - Save processed dataframe columns with annotations to an HDF5 file. - - Parameters: - df (pd.DataFrame): DataFrame containing processed time series. - annotator (): Annotator object with get_metadata method. - output_filename (str): Path to the source HDF5 file. - """ - # Convert datetime columns to string - datetime_cols = df.select_dtypes(include=['datetime64']).columns - - if list(datetime_cols): - df[datetime_cols] = df[datetime_cols].map(str) - - # Convert dataframe to structured array - icad_data_table = utils.convert_dataframe_to_np_structured_array(df) - - # Get metadata - metadata_dict = annotator.get_metadata() - - # Prepare project level attributes to be added at the root level - - project_level_attributes = metadata_dict['metadata']['project'] - - # Prepare high-level attributes - high_level_attributes = { - 'parent_files': metadata_dict['parent_files'], - **metadata_dict['metadata']['sample'], - **metadata_dict['metadata']['environment'], - **metadata_dict['metadata']['instruments'] - } - - # Prepare data level attributes - data_level_attributes = metadata_dict['metadata']['datasets'] - - for key, value in data_level_attributes.items(): - if isinstance(value,dict): - data_level_attributes[key] = utils.convert_attrdict_to_np_structured_array(value) - - - # Prepare file dictionary - file_dict = { - 'name': project_level_attributes['processing_file'], - 'attributes_dict': high_level_attributes, - 'datasets': [{ - 'name': "data_table", - 'data': icad_data_table, - 'shape': icad_data_table.shape, - 'attributes': data_level_attributes - }] - } - - # Check if the file exists - if os.path.exists(output_filename): - mode = "a" - print(f"File {output_filename} exists. Opening in append mode.") - else: - mode = "w" - print(f"File {output_filename} does not exist. Creating a new file.") - - - # Write to HDF5 - with h5py.File(output_filename, mode) as h5file: - # Add project level attributes at the root/top level - h5file.attrs.update(project_level_attributes) - __transfer_file_dict_to_hdf5(h5file, '/', file_dict)
- - -#if __name__ == '__main__': -
- -
-
- -
-
-
-
- - - + + + + + + src.hdf5_lib — DIMA 1.0.0 documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for src.hdf5_lib

+import sys
+import os
+root_dir = os.path.abspath(os.curdir)
+sys.path.append(root_dir)
+
+import pandas as pd
+import numpy as np
+import h5py
+import logging
+
+import utils.g5505_utils as utils
+import instruments.readers.filereader_registry as filereader_registry
+
+ 
+   
+def __transfer_file_dict_to_hdf5(h5file, group_name, file_dict):
+    """
+    Transfers data from a file_dict to an HDF5 file.
+
+    Parameters
+    ----------
+    h5file : h5py.File
+        HDF5 file object where the data will be written.
+    group_name : str
+        Name of the HDF5 group where data will be stored.
+    file_dict : dict
+        Dictionary containing file data to be transferred. Required structure:
+        {
+            'name': str,
+            'attributes_dict': dict,
+            'datasets': [
+                {
+                    'name': str,
+                    'data': array-like,
+                    'shape': tuple,
+                    'attributes': dict (optional)
+                },
+                ...
+            ]
+        }
+
+    Returns
+    -------
+    None
+    """
+
+    if not file_dict:
+        return
+
+    try:
+        # Create group and add their attributes
+        group = h5file[group_name].create_group(name=file_dict['name'])
+        # Add group attributes                                
+        group.attrs.update(file_dict['attributes_dict'])
+        
+        # Add datasets to the just created group
+        for dataset in file_dict['datasets']:
+            dataset_obj = group.create_dataset(
+                name=dataset['name'], 
+                data=dataset['data'],
+                shape=dataset['shape']
+            )
+            
+            # Add dataset's attributes                                
+            attributes = dataset.get('attributes', {})
+            dataset_obj.attrs.update(attributes)
+        group.attrs['last_update_date'] = utils.created_at().encode('utf-8')
+    except Exception as inst: 
+        print(inst) 
+        logging.error('Failed to transfer data into HDF5: %s', inst)
+
+def __copy_file_in_group(source_file_path, dest_file_obj : h5py.File, dest_group_name, work_with_copy : bool = True):
+    # Create copy of original file to avoid possible file corruption and work with it.
+
+    if work_with_copy:
+        tmp_file_path = utils.make_file_copy(source_file_path)
+    else:
+        tmp_file_path = source_file_path
+
+    # Open backup h5 file and copy complet filesystem directory onto a group in h5file
+    with h5py.File(tmp_file_path,'r') as src_file:
+        dest_file_obj.copy(source= src_file['/'], dest= dest_group_name)
+
+    if 'tmp_files' in tmp_file_path:
+        os.remove(tmp_file_path)
+
+
+[docs] +def create_hdf5_file_from_filesystem_path(path_to_input_directory: str, + path_to_filenames_dict: dict = None, + select_dir_keywords : list = [], + root_metadata_dict : dict = {}, mode = 'w'): + + """ + Creates an .h5 file with name "output_filename" that preserves the directory tree (or folder structure) + of a given filesystem path. + + The data integration capabilities are limited by our file reader, which can only access data from a list of + admissible file formats. These, however, can be extended. Directories are groups in the resulting HDF5 file. + Files are formatted as composite objects consisting of a group, file, and attributes. + + Parameters + ---------- + output_filename : str + Name of the output HDF5 file. + path_to_input_directory : str + Path to root directory, specified with forward slashes, e.g., path/to/root. + + path_to_filenames_dict : dict, optional + A pre-processed dictionary where keys are directory paths on the input directory's tree and values are lists of files. + If provided, 'input_file_system_path' is ignored. + + select_dir_keywords : list + List of string elements to consider or select only directory paths that contain + a word in 'select_dir_keywords'. When empty, all directory paths are considered + to be included in the HDF5 file group hierarchy. + root_metadata_dict : dict + Metadata to include at the root level of the HDF5 file. + + mode : str + 'w' create File, truncate if it exists, or 'r+' read/write, File must exists. By default, mode = "w". + + Returns + ------- + output_filename : str + Path to the created HDF5 file. + """ + + + if not mode in ['w','r+']: + raise ValueError(f'Parameter mode must take values in ["w","r+"]') + + if not '/' in path_to_input_directory: + raise ValueError('path_to_input_directory needs to be specified using forward slashes "/".' ) + + #path_to_output_directory = os.path.join(path_to_input_directory,'..') + path_to_input_directory = os.path.normpath(path_to_input_directory).rstrip(os.sep) + + + for i, keyword in enumerate(select_dir_keywords): + select_dir_keywords[i] = keyword.replace('/',os.sep) + + if not path_to_filenames_dict: + # On dry_run=True, returns path to files dictionary of the output directory without making a actual copy of the input directory. + # Therefore, there wont be a copying conflict by setting up input and output directories the same + path_to_filenames_dict = utils.copy_directory_with_contraints(input_dir_path=path_to_input_directory, + output_dir_path=path_to_input_directory, + dry_run=True) + # Set input_directory as copied input directory + root_dir = path_to_input_directory + path_to_output_file = path_to_input_directory.rstrip(os.path.sep) + '.h5' + + with h5py.File(path_to_output_file, mode=mode, track_order=True) as h5file: + + number_of_dirs = len(path_to_filenames_dict.keys()) + dir_number = 1 + for dirpath, filtered_filenames_list in path_to_filenames_dict.items(): + + start_message = f'Starting to transfer files in directory: {dirpath}' + end_message = f'\nCompleted transferring files in directory: {dirpath}' + # Print and log the start message + print(start_message) + logging.info(start_message) + + # Check if filtered_filenames_list is nonempty. TODO: This is perhaps redundant by design of path_to_filenames_dict. + if not filtered_filenames_list: + continue + + group_name = dirpath.replace(os.sep,'/') + group_name = group_name.replace(root_dir.replace(os.sep,'/') + '/', '/') + + # Flatten group name to one level + if select_dir_keywords: + offset = sum([len(i.split(os.sep)) if i in dirpath else 0 for i in select_dir_keywords]) + else: + offset = 1 + tmp_list = group_name.split('/') + if len(tmp_list) > offset+1: + group_name = '/'.join([tmp_list[i] for i in range(offset+1)]) + + # Group hierarchy is implicitly defined by the forward slashes + if not group_name in h5file.keys(): + h5file.create_group(group_name) + h5file[group_name].attrs['creation_date'] = utils.created_at().encode('utf-8') + #h5file[group_name].attrs.create(name='filtered_file_list',data=convert_string_to_bytes(filtered_filename_list)) + #h5file[group_name].attrs.create(name='file_list',data=convert_string_to_bytes(filenames_list)) + else: + print(group_name,' was already created.') + + for filenumber, filename in enumerate(filtered_filenames_list): + + #file_ext = os.path.splitext(filename)[1] + #try: + + # hdf5 path to filename group + dest_group_name = f'{group_name}/{filename}' + + if not 'h5' in filename: + #file_dict = config_file.select_file_readers(group_id)[file_ext](os.path.join(dirpath,filename)) + #file_dict = ext_to_reader_dict[file_ext](os.path.join(dirpath,filename)) + file_dict = filereader_registry.select_file_reader(dest_group_name)(os.path.join(dirpath,filename)) + + __transfer_file_dict_to_hdf5(h5file, group_name, file_dict) + + else: + source_file_path = os.path.join(dirpath,filename) + dest_file_obj = h5file + #group_name +'/'+filename + #ext_to_reader_dict[file_ext](source_file_path, dest_file_obj, dest_group_name) + #g5505f_reader.select_file_reader(dest_group_name)(source_file_path, dest_file_obj, dest_group_name) + __copy_file_in_group(source_file_path, dest_file_obj, dest_group_name, False) + + # Update the progress bar and log the end message + utils.progressBar(dir_number, number_of_dirs, end_message) + logging.info(end_message) + dir_number = dir_number + 1 + + + + if len(root_metadata_dict.keys())>0: + for key, value in root_metadata_dict.items(): + #if key in h5file.attrs: + # del h5file.attrs[key] + h5file.attrs.create(key, value) + #annotate_root_dir(output_filename,root_metadata_dict) + + + #output_yml_filename_path = hdf5_vis.take_yml_snapshot_of_hdf5_file(output_filename) + + return path_to_output_file #, output_yml_filename_path
+ + +
+[docs] +def save_processed_dataframe_to_hdf5(df, annotator, output_filename): # src_hdf5_path, script_date, script_name): + """ + Save processed dataframe columns with annotations to an HDF5 file. + + Parameters: + df (pd.DataFrame): DataFrame containing processed time series. + annotator (): Annotator object with get_metadata method. + output_filename (str): Path to the source HDF5 file. + """ + # Convert datetime columns to string + datetime_cols = df.select_dtypes(include=['datetime64']).columns + + if list(datetime_cols): + df[datetime_cols] = df[datetime_cols].map(str) + + # Convert dataframe to structured array + icad_data_table = utils.convert_dataframe_to_np_structured_array(df) + + # Get metadata + metadata_dict = annotator.get_metadata() + + # Prepare project level attributes to be added at the root level + + project_level_attributes = metadata_dict['metadata']['project'] + + # Prepare high-level attributes + high_level_attributes = { + 'parent_files': metadata_dict['parent_files'], + **metadata_dict['metadata']['sample'], + **metadata_dict['metadata']['environment'], + **metadata_dict['metadata']['instruments'] + } + + # Prepare data level attributes + data_level_attributes = metadata_dict['metadata']['datasets'] + + for key, value in data_level_attributes.items(): + if isinstance(value,dict): + data_level_attributes[key] = utils.convert_attrdict_to_np_structured_array(value) + + + # Prepare file dictionary + file_dict = { + 'name': project_level_attributes['processing_file'], + 'attributes_dict': high_level_attributes, + 'datasets': [{ + 'name': "data_table", + 'data': icad_data_table, + 'shape': icad_data_table.shape, + 'attributes': data_level_attributes + }] + } + + # Check if the file exists + if os.path.exists(output_filename): + mode = "a" + print(f"File {output_filename} exists. Opening in append mode.") + else: + mode = "w" + print(f"File {output_filename} does not exist. Creating a new file.") + + + # Write to HDF5 + with h5py.File(output_filename, mode) as h5file: + # Add project level attributes at the root/top level + h5file.attrs.update(project_level_attributes) + __transfer_file_dict_to_hdf5(h5file, '/', file_dict)
+ + +#if __name__ == '__main__': +
+ +
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/docs/build/html/_modules/src/hdf5_ops.html b/docs/build/html/_modules/src/hdf5_ops.html index 7a45dc3..2d55089 100644 --- a/docs/build/html/_modules/src/hdf5_ops.html +++ b/docs/build/html/_modules/src/hdf5_ops.html @@ -1,832 +1,832 @@ - - - - - - src.hdf5_ops — DIMA 1.0.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for src.hdf5_ops

-import sys
-import os
-
-try:
-    thisFilePath = os.path.abspath(__file__)
-except NameError:
-    print("Error: __file__ is not available. Ensure the script is being run from a file.")
-    print("[Notice] Path to DIMA package may not be resolved properly.")
-    thisFilePath = os.getcwd()  # Use current directory or specify a default
-
-dimaPath = os.path.normpath(os.path.join(thisFilePath, "..",'..'))  # Move up to project root
-
-if dimaPath not in sys.path:  # Avoid duplicate entries
-    sys.path.append(dimaPath)
-
-
-import h5py
-import pandas as pd
-import numpy as np
-
-import utils.g5505_utils as utils
-import src.hdf5_writer as hdf5_lib
-import logging
-import datetime
-
-import h5py
-
-import yaml
-import json
-import copy
-
-
-[docs] -class HDF5DataOpsManager(): - - """ - A class to handle HDF5 fundamental middle level file operations to power data updates, metadata revision, and data analysis - with hdf5 files encoding multi-instrument experimental campaign data. - - Parameters: - ----------- - path_to_file : str - path/to/hdf5file. - mode : str - 'r' or 'r+' read or read/write mode only when file exists - """ - def __init__(self, file_path, mode = 'r+') -> None: - - # Class attributes - if mode in ['r','r+']: - self.mode = mode - self.file_path = file_path - self.file_obj = None - #self._open_file() - self.dataset_metadata_df = None - - # Define private methods - - # Define public methods - -
-[docs] - def load_file_obj(self): - if self.file_obj is None: - self.file_obj = h5py.File(self.file_path, self.mode)
- - -
-[docs] - def unload_file_obj(self): - if self.file_obj: - self.file_obj.flush() # Ensure all data is written to disk - self.file_obj.close() - self.file_obj = None
- - -
-[docs] - def extract_and_load_dataset_metadata(self): - - def __get_datasets(name, obj, list_of_datasets): - if isinstance(obj,h5py.Dataset): - list_of_datasets.append(name) - #print(f'Adding dataset: {name}') #tail: {head} head: {tail}') - list_of_datasets = [] - - if self.file_obj is None: - raise RuntimeError("File object is not loaded. Please load the HDF5 file using the 'load_file_obj' method before attempting to extract datasets.") - - try: - - list_of_datasets = [] - - self.file_obj.visititems(lambda name, obj: __get_datasets(name, obj, list_of_datasets)) - - dataset_metadata_df = pd.DataFrame({'dataset_name': list_of_datasets}) - dataset_metadata_df['parent_instrument'] = dataset_metadata_df['dataset_name'].apply(lambda x: x.split('/')[-3]) - dataset_metadata_df['parent_file'] = dataset_metadata_df['dataset_name'].apply(lambda x: x.split('/')[-2]) - - self.dataset_metadata_df = dataset_metadata_df - - except Exception as e: - - self.unload_file_obj() - print(f"An unexpected error occurred: {e}. File object will be unloaded.")
- - - - - -
-[docs] - def extract_dataset_as_dataframe(self,dataset_name): - """ - returns a copy of the dataset content in the form of dataframe when possible or numpy array - """ - if self.file_obj is None: - raise RuntimeError("File object is not loaded. Please load the HDF5 file using the 'load_file_obj' method before attempting to extract datasets.") - - dataset_obj = self.file_obj[dataset_name] - # Read dataset content from dataset obj - data = dataset_obj[...] - # The above statement can be understood as follows: - # data = np.empty(shape=dataset_obj.shape, - # dtype=dataset_obj.dtype) - # dataset_obj.read_direct(data) - - try: - return pd.DataFrame(data) - except ValueError as e: - logging.error(f"Failed to convert dataset '{dataset_name}' to DataFrame: {e}. Instead, dataset will be returned as Numpy array.") - return data # 'data' is a NumPy array here - except Exception as e: - self.unload_file_obj() - print(f"An unexpected error occurred: {e}. Returning None and unloading file object") - return None
- - - # Define metadata revision methods: append(), update(), delete(), and rename(). - -
-[docs] - def append_metadata(self, obj_name, annotation_dict): - """ - Appends metadata attributes to the specified object (obj_name) based on the provided annotation_dict. - - This method ensures that the provided metadata attributes do not overwrite any existing ones. If an attribute already exists, - a ValueError is raised. The function supports storing scalar values (int, float, str) and compound values such as dictionaries - that are converted into NumPy structured arrays before being added to the metadata. - - Parameters: - ----------- - obj_name: str - Path to the target object (dataset or group) within the HDF5 file. - - annotation_dict: dict - A dictionary where the keys represent new attribute names (strings), and the values can be: - - Scalars: int, float, or str. - - Compound values (dictionaries) for more complex metadata, which are converted to NumPy structured arrays. - Example of a compound value: - - Example: - ---------- - annotation_dict = { - "relative_humidity": { - "value": 65, - "units": "percentage", - "range": "[0,100]", - "definition": "amount of water vapor present ..." - } - } - """ - - if self.file_obj is None: - raise RuntimeError("File object is not loaded. Please load the HDF5 file using the 'load_file_obj' method before attempting to modify it.") - - # Create a copy of annotation_dict to avoid modifying the original - annotation_dict_copy = copy.deepcopy(annotation_dict) - - try: - obj = self.file_obj[obj_name] - - # Check if any attribute already exists - if any(key in obj.attrs for key in annotation_dict_copy.keys()): - raise ValueError("Make sure the provided (key, value) pairs are not existing metadata elements or attributes. To modify or delete existing attributes use .modify_annotation() or .delete_annotation()") - - # Process the dictionary values and convert them to structured arrays if needed - for key, value in annotation_dict_copy.items(): - if isinstance(value, dict): - # Convert dictionaries to NumPy structured arrays for complex attributes - annotation_dict_copy[key] = utils.convert_attrdict_to_np_structured_array(value) - - # Update the object's attributes with the new metadata - obj.attrs.update(annotation_dict_copy) - - except Exception as e: - self.unload_file_obj() - print(f"An unexpected error occurred: {e}. The file object has been properly closed.")
- - - -
-[docs] - def update_metadata(self, obj_name, annotation_dict): - """ - Updates the value of existing metadata attributes of the specified object (obj_name) based on the provided annotation_dict. - - The function disregards non-existing attributes and suggests to use the append_metadata() method to include those in the metadata. - - Parameters: - ----------- - obj_name : str - Path to the target object (dataset or group) within the HDF5 file. - - annotation_dict: dict - A dictionary where the keys represent existing attribute names (strings), and the values can be: - - Scalars: int, float, or str. - - Compound values (dictionaries) for more complex metadata, which are converted to NumPy structured arrays. - Example of a compound value: - - Example: - ---------- - annotation_dict = { - "relative_humidity": { - "value": 65, - "units": "percentage", - "range": "[0,100]", - "definition": "amount of water vapor present ..." - } - } - - - """ - - if self.file_obj is None: - raise RuntimeError("File object is not loaded. Please load the HDF5 file using the 'load_file_obj' method before attempting to modify it.") - - update_dict = {} - - try: - - obj = self.file_obj[obj_name] - for key, value in annotation_dict.items(): - if key in obj.attrs: - if isinstance(value, dict): - update_dict[key] = utils.convert_attrdict_to_np_structured_array(value) - else: - update_dict[key] = value - else: - # Optionally, log or warn about non-existing keys being ignored. - print(f"Warning: Key '{key}' does not exist and will be ignored.") - - obj.attrs.update(update_dict) - - except Exception as e: - self.unload_file_obj() - print(f"An unexpected error occurred: {e}. The file object has been properly closed.")
- - -
-[docs] - def delete_metadata(self, obj_name, annotation_dict): - """ - Deletes metadata attributes of the specified object (obj_name) based on the provided annotation_dict. - - Parameters: - ----------- - obj_name: str - Path to the target object (dataset or group) within the HDF5 file. - - annotation_dict: dict - Dictionary where keys represent attribute names, and values should be dictionaries containing - {"delete": True} to mark them for deletion. - - Example: - -------- - annotation_dict = {"attr_to_be_deleted": {"delete": True}} - - Behavior: - --------- - - Deletes the specified attributes from the object's metadata if marked for deletion. - - Issues a warning if the attribute is not found or not marked for deletion. - """ - - if self.file_obj is None: - raise RuntimeError("File object is not loaded. Please load the HDF5 file using the 'load_file_obj' method before attempting to modify it.") - - try: - obj = self.file_obj[obj_name] - for attr_key, value in annotation_dict.items(): - if attr_key in obj.attrs: - if isinstance(value, dict) and value.get('delete', False): - obj.attrs.__delitem__(attr_key) - else: - msg = f"Warning: Value for key '{attr_key}' is not marked for deletion or is invalid." - print(msg) - else: - msg = f"Warning: Key '{attr_key}' does not exist in metadata." - print(msg) - - except Exception as e: - self.unload_file_obj() - print(f"An unexpected error occurred: {e}. The file object has been properly closed.")
- - - -
-[docs] - def rename_metadata(self, obj_name, renaming_map): - """ - Renames metadata attributes of the specified object (obj_name) based on the provided renaming_map. - - Parameters: - ----------- - obj_name: str - Path to the target object (dataset or group) within the HDF5 file. - - renaming_map: dict - A dictionary where keys are current attribute names (strings), and values are the new attribute names (strings or byte strings) to rename to. - - Example: - -------- - renaming_map = { - "old_attr_name": "new_attr_name", - "old_attr_2": "new_attr_2" - } - - """ - - if self.file_obj is None: - raise RuntimeError("File object is not loaded. Please load the HDF5 file using the 'load_file_obj' method before attempting to modify it.") - - try: - obj = self.file_obj[obj_name] - # Iterate over the renaming_map to process renaming - for old_attr, new_attr in renaming_map.items(): - if old_attr in obj.attrs: - # Get the old attribute's value - attr_value = obj.attrs[old_attr] - - # Create a new attribute with the new name - obj.attrs.create(new_attr, data=attr_value) - - # Delete the old attribute - obj.attrs.__delitem__(old_attr) - else: - # Skip if the old attribute doesn't exist - msg = f"Skipping: Attribute '{old_attr}' does not exist." - print(msg) # Optionally, replace with warnings.warn(msg) - except Exception as e: - self.unload_file_obj() - print( - f"An unexpected error occurred: {e}. The file object has been properly closed. " - "Please ensure that 'obj_name' exists in the file, and that the keys in 'renaming_map' are valid attributes of the object." - ) - - self.unload_file_obj()
- - -
-[docs] - def get_metadata(self, obj_path): - """ Get file attributes from object at path = obj_path. For example, - obj_path = '/' will get root level attributes or metadata. - """ - try: - # Access the attributes for the object at the given path - metadata_dict = self.file_obj[obj_path].attrs - except KeyError: - # Handle the case where the path doesn't exist - logging.error(f'Invalid object path: {obj_path}') - metadata_dict = {} - - return metadata_dict
- - - -
-[docs] - def reformat_datetime_column(self, dataset_name, column_name, src_format, desired_format='%Y-%m-%d %H:%M:%S.%f'): - # Access the dataset - dataset = self.file_obj[dataset_name] - - # Read the column data into a pandas Series and decode bytes to strings - dt_column_data = pd.Series(dataset[column_name][:]).apply(lambda x: x.decode() ) - - # Convert to datetime using the source format - dt_column_data = pd.to_datetime(dt_column_data, format=src_format, errors = 'coerce') - - # Reformat datetime objects to the desired format as strings - dt_column_data = dt_column_data.dt.strftime(desired_format) - - # Encode the strings back to bytes - #encoded_data = dt_column_data.apply(lambda x: x.encode() if not pd.isnull(x) else 'N/A').to_numpy() - - # Update the dataset in place - #dataset[column_name][:] = encoded_data - - # Convert byte strings to datetime objects - #timestamps = [datetime.datetime.strptime(a.decode(), src_format).strftime(desired_format) for a in dt_column_data] - - #datetime.strptime('31/01/22 23:59:59.999999', - # '%d/%m/%y %H:%M:%S.%f') - - #pd.to_datetime( - # np.array([a.decode() for a in dt_column_data]), - # format=src_format, - # errors='coerce' - #) - - - # Standardize the datetime format - #standardized_time = datetime.strftime(desired_format) - - # Convert to byte strings to store back in the HDF5 dataset - #standardized_time_bytes = np.array([s.encode() for s in timestamps]) - - # Update the column in the dataset (in-place update) - # TODO: make this a more secure operation - #dataset[column_name][:] = standardized_time_bytes - - #return np.array(timestamps) - return dt_column_data.to_numpy()
- - - # Define data append operations: append_dataset(), and update_file() - -
-[docs] - def append_dataset(self,dataset_dict, group_name): - - # Parse value into HDF5 admissible type - for key in dataset_dict['attributes'].keys(): - value = dataset_dict['attributes'][key] - if isinstance(value, dict): - dataset_dict['attributes'][key] = utils.convert_attrdict_to_np_structured_array(value) - - if not group_name in self.file_obj: - self.file_obj.create_group(group_name, track_order=True) - self.file_obj[group_name].attrs['creation_date'] = utils.created_at().encode("utf-8") - - self.file_obj[group_name].create_dataset(dataset_dict['name'], data=dataset_dict['data']) - self.file_obj[group_name][dataset_dict['name']].attrs.update(dataset_dict['attributes']) - self.file_obj[group_name].attrs['last_update_date'] = utils.created_at().encode("utf-8")
- - -
-[docs] - def update_file(self, path_to_append_dir): - # Split the reference file path and the append directory path into directories and filenames - ref_tail, ref_head = os.path.split(self.file_path) - ref_head_filename, head_ext = os.path.splitext(ref_head) - tail, head = os.path.split(path_to_append_dir) - - - # Ensure the append directory is in the same directory as the reference file and has the same name (without extension) - if not (ref_tail == tail and ref_head_filename == head): - raise ValueError("The append directory must be in the same directory as the reference HDF5 file and have the same name without the extension.") - - # Close the file if it's already open - if self.file_obj is not None: - self.unload_file_obj() - - # Attempt to open the file in 'r+' mode for appending - try: - hdf5_lib.create_hdf5_file_from_filesystem_path(path_to_append_dir, mode='r+') - except FileNotFoundError: - raise FileNotFoundError(f"Reference HDF5 file '{self.file_path}' not found.") - except OSError as e: - raise OSError(f"Error opening HDF5 file: {e}")
-
- - - - -
-[docs] -def get_parent_child_relationships(file: h5py.File): - - nodes = ['/'] - parent = [''] - #values = [file.attrs['count']] - # TODO: maybe we should make this more general and not dependent on file_list attribute? - #if 'file_list' in file.attrs.keys(): - # values = [len(file.attrs['file_list'])] - #else: - # values = [1] - values = [len(file.keys())] - - def node_visitor(name,obj): - if name.count('/') <=2: - nodes.append(obj.name) - parent.append(obj.parent.name) - #nodes.append(os.path.split(obj.name)[1]) - #parent.append(os.path.split(obj.parent.name)[1]) - - if isinstance(obj,h5py.Dataset):# or not 'file_list' in obj.attrs.keys(): - values.append(1) - else: - print(obj.name) - try: - values.append(len(obj.keys())) - except: - values.append(0) - - file.visititems(node_visitor) - - return nodes, parent, values
- - - -def __print_metadata__(name, obj, folder_depth, yaml_dict): - - """ - Extracts metadata from HDF5 groups and datasets and organizes them into a dictionary with compact representation. - - Parameters: - ----------- - name (str): Name of the HDF5 object being inspected. - obj (h5py.Group or h5py.Dataset): The HDF5 object (Group or Dataset). - folder_depth (int): Maximum depth of folders to explore. - yaml_dict (dict): Dictionary to populate with metadata. - """ - # Process only objects within the specified folder depth - if len(obj.name.split('/')) <= folder_depth: # and ".h5" not in obj.name: - name_to_list = obj.name.split('/') - name_head = name_to_list[-1] if not name_to_list[-1]=='' else obj.name - - if isinstance(obj, h5py.Group): # Handle groups - # Convert attributes to a YAML/JSON serializable format - attr_dict = {key: utils.to_serializable_dtype(val) for key, val in obj.attrs.items()} - - # Initialize the group dictionary - group_dict = {"name": name_head, "attributes": attr_dict} - - # Handle group members compactly - #subgroups = [member_name for member_name in obj if isinstance(obj[member_name], h5py.Group)] - #datasets = [member_name for member_name in obj if isinstance(obj[member_name], h5py.Dataset)] - - # Summarize groups and datasets - #group_dict["content_summary"] = { - # "group_count": len(subgroups), - # "group_preview": subgroups[:3] + (["..."] if len(subgroups) > 3 else []), - # "dataset_count": len(datasets), - # "dataset_preview": datasets[:3] + (["..."] if len(datasets) > 3 else []) - #} - - yaml_dict[obj.name] = group_dict - - elif isinstance(obj, h5py.Dataset): # Handle datasets - # Convert attributes to a YAML/JSON serializable format - attr_dict = {key: utils.to_serializable_dtype(val) for key, val in obj.attrs.items()} - - dataset_dict = {"name": name_head, "attributes": attr_dict} - - yaml_dict[obj.name] = dataset_dict - - - -
-[docs] -def serialize_metadata(input_filename_path, folder_depth: int = 4, output_format: str = 'yaml') -> str: - """ - Serialize metadata from an HDF5 file into YAML or JSON format. - - Parameters - ---------- - input_filename_path : str - The path to the input HDF5 file. - folder_depth : int, optional - The folder depth to control how much of the HDF5 file hierarchy is traversed (default is 4). - output_format : str, optional - The format to serialize the output, either 'yaml' or 'json' (default is 'yaml'). - - Returns - ------- - str - The output file path where the serialized metadata is stored (either .yaml or .json). - - """ - - # Choose the appropriate output format (YAML or JSON) - if output_format not in ['yaml', 'json']: - raise ValueError("Unsupported format. Please choose either 'yaml' or 'json'.") - - # Initialize dictionary to store YAML/JSON data - yaml_dict = {} - - # Split input file path to get the output file's base name - output_filename_tail, ext = os.path.splitext(input_filename_path) - - # Open the HDF5 file and extract metadata - with h5py.File(input_filename_path, 'r') as f: - # Convert attribute dict to a YAML/JSON serializable dict - #attrs_dict = {key: utils.to_serializable_dtype(val) for key, val in f.attrs.items()} - #yaml_dict[f.name] = { - # "name": f.name, - # "attributes": attrs_dict, - # "datasets": {} - #} - __print_metadata__(f.name, f, folder_depth, yaml_dict) - # Traverse HDF5 file hierarchy and add datasets - f.visititems(lambda name, obj: __print_metadata__(name, obj, folder_depth, yaml_dict)) - - - # Serialize and write the data - output_file_path = output_filename_tail + '.' + output_format - with open(output_file_path, 'w') as output_file: - if output_format == 'json': - json_output = json.dumps(yaml_dict, indent=4, sort_keys=False) - output_file.write(json_output) - elif output_format == 'yaml': - yaml_output = yaml.dump(yaml_dict, sort_keys=False) - output_file.write(yaml_output) - - return output_file_path
- - - -
-[docs] -def get_groups_at_a_level(file: h5py.File, level: str): - - groups = [] - def node_selector(name, obj): - if name.count('/') == level: - print(name) - groups.append(obj.name) - - file.visititems(node_selector) - #file.visititems() - return groups
- - -
-[docs] -def read_mtable_as_dataframe(filename): - - """ - Reconstruct a MATLAB Table encoded in a .h5 file as a Pandas DataFrame. - - This function reads a .h5 file containing a MATLAB Table and reconstructs it as a Pandas DataFrame. - The input .h5 file contains one group per row of the MATLAB Table. Each group stores the table's - dataset-like variables as Datasets, while categorical and numerical variables are represented as - attributes of the respective group. - - To ensure homogeneity of data columns, the DataFrame is constructed column-wise. - - Parameters - ---------- - filename : str - The name of the .h5 file. This may include the file's location and path information. - - Returns - ------- - pd.DataFrame - The MATLAB Table reconstructed as a Pandas DataFrame. - """ - - - #contructs dataframe by filling out entries columnwise. This way we can ensure homogenous data columns""" - - with h5py.File(filename,'r') as file: - - # Define group's attributes and datasets. This should hold - # for all groups. TODO: implement verification and noncompliance error if needed. - group_list = list(file.keys()) - group_attrs = list(file[group_list[0]].attrs.keys()) - # - column_attr_names = [item[item.find('_')+1::] for item in group_attrs] - column_attr_names_idx = [int(item[4:(item.find('_'))]) for item in group_attrs] - - group_datasets = list(file[group_list[0]].keys()) if not 'DS_EMPTY' in file[group_list[0]].keys() else [] - # - column_dataset_names = [file[group_list[0]][item].attrs['column_name'] for item in group_datasets] - column_dataset_names_idx = [int(item[2:]) for item in group_datasets] - - - # Define data_frame as group_attrs + group_datasets - #pd_series_index = group_attrs + group_datasets - pd_series_index = column_attr_names + column_dataset_names - - output_dataframe = pd.DataFrame(columns=pd_series_index,index=group_list) - - tmp_col = [] - - for meas_prop in group_attrs + group_datasets: - if meas_prop in group_attrs: - column_label = meas_prop[meas_prop.find('_')+1:] - # Create numerical or categorical column from group's attributes - tmp_col = [file[group_key].attrs[meas_prop][()][0] for group_key in group_list] - else: - # Create dataset column from group's datasets - column_label = file[group_list[0] + '/' + meas_prop].attrs['column_name'] - #tmp_col = [file[group_key + '/' + meas_prop][()][0] for group_key in group_list] - tmp_col = [file[group_key + '/' + meas_prop][()] for group_key in group_list] - - output_dataframe.loc[:,column_label] = tmp_col - - return output_dataframe
- - -if __name__ == "__main__": - if len(sys.argv) < 5: - print("Usage: python hdf5_ops.py serialize <path/to/target_file.hdf5> <folder_depth : int = 2> <format=json|yaml>") - sys.exit(1) - - if sys.argv[1] == 'serialize': - input_hdf5_file = sys.argv[2] - folder_depth = int(sys.argv[3]) - file_format = sys.argv[4] - - try: - # Call the serialize_metadata function and capture the output path - path_to_file = serialize_metadata(input_hdf5_file, - folder_depth = folder_depth, - output_format=file_format) - print(f"Metadata serialized to {path_to_file}") - except Exception as e: - print(f"An error occurred during serialization: {e}") - sys.exit(1) - - #run(sys.argv[2]) - -
- -
-
- -
-
-
-
- - - + + + + + + src.hdf5_ops — DIMA 1.0.0 documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for src.hdf5_ops

+import sys
+import os
+
+try:
+    thisFilePath = os.path.abspath(__file__)
+except NameError:
+    print("Error: __file__ is not available. Ensure the script is being run from a file.")
+    print("[Notice] Path to DIMA package may not be resolved properly.")
+    thisFilePath = os.getcwd()  # Use current directory or specify a default
+
+dimaPath = os.path.normpath(os.path.join(thisFilePath, "..",'..'))  # Move up to project root
+
+if dimaPath not in sys.path:  # Avoid duplicate entries
+    sys.path.append(dimaPath)
+
+
+import h5py
+import pandas as pd
+import numpy as np
+
+import utils.g5505_utils as utils
+import src.hdf5_writer as hdf5_lib
+import logging
+import datetime
+
+import h5py
+
+import yaml
+import json
+import copy
+
+
+[docs] +class HDF5DataOpsManager(): + + """ + A class to handle HDF5 fundamental middle level file operations to power data updates, metadata revision, and data analysis + with hdf5 files encoding multi-instrument experimental campaign data. + + Parameters: + ----------- + path_to_file : str + path/to/hdf5file. + mode : str + 'r' or 'r+' read or read/write mode only when file exists + """ + def __init__(self, file_path, mode = 'r+') -> None: + + # Class attributes + if mode in ['r','r+']: + self.mode = mode + self.file_path = file_path + self.file_obj = None + #self._open_file() + self.dataset_metadata_df = None + + # Define private methods + + # Define public methods + +
+[docs] + def load_file_obj(self): + if self.file_obj is None: + self.file_obj = h5py.File(self.file_path, self.mode)
+ + +
+[docs] + def unload_file_obj(self): + if self.file_obj: + self.file_obj.flush() # Ensure all data is written to disk + self.file_obj.close() + self.file_obj = None
+ + +
+[docs] + def extract_and_load_dataset_metadata(self): + + def __get_datasets(name, obj, list_of_datasets): + if isinstance(obj,h5py.Dataset): + list_of_datasets.append(name) + #print(f'Adding dataset: {name}') #tail: {head} head: {tail}') + list_of_datasets = [] + + if self.file_obj is None: + raise RuntimeError("File object is not loaded. Please load the HDF5 file using the 'load_file_obj' method before attempting to extract datasets.") + + try: + + list_of_datasets = [] + + self.file_obj.visititems(lambda name, obj: __get_datasets(name, obj, list_of_datasets)) + + dataset_metadata_df = pd.DataFrame({'dataset_name': list_of_datasets}) + dataset_metadata_df['parent_instrument'] = dataset_metadata_df['dataset_name'].apply(lambda x: x.split('/')[-3]) + dataset_metadata_df['parent_file'] = dataset_metadata_df['dataset_name'].apply(lambda x: x.split('/')[-2]) + + self.dataset_metadata_df = dataset_metadata_df + + except Exception as e: + + self.unload_file_obj() + print(f"An unexpected error occurred: {e}. File object will be unloaded.")
+ + + + + +
+[docs] + def extract_dataset_as_dataframe(self,dataset_name): + """ + returns a copy of the dataset content in the form of dataframe when possible or numpy array + """ + if self.file_obj is None: + raise RuntimeError("File object is not loaded. Please load the HDF5 file using the 'load_file_obj' method before attempting to extract datasets.") + + dataset_obj = self.file_obj[dataset_name] + # Read dataset content from dataset obj + data = dataset_obj[...] + # The above statement can be understood as follows: + # data = np.empty(shape=dataset_obj.shape, + # dtype=dataset_obj.dtype) + # dataset_obj.read_direct(data) + + try: + return pd.DataFrame(data) + except ValueError as e: + logging.error(f"Failed to convert dataset '{dataset_name}' to DataFrame: {e}. Instead, dataset will be returned as Numpy array.") + return data # 'data' is a NumPy array here + except Exception as e: + self.unload_file_obj() + print(f"An unexpected error occurred: {e}. Returning None and unloading file object") + return None
+ + + # Define metadata revision methods: append(), update(), delete(), and rename(). + +
+[docs] + def append_metadata(self, obj_name, annotation_dict): + """ + Appends metadata attributes to the specified object (obj_name) based on the provided annotation_dict. + + This method ensures that the provided metadata attributes do not overwrite any existing ones. If an attribute already exists, + a ValueError is raised. The function supports storing scalar values (int, float, str) and compound values such as dictionaries + that are converted into NumPy structured arrays before being added to the metadata. + + Parameters: + ----------- + obj_name: str + Path to the target object (dataset or group) within the HDF5 file. + + annotation_dict: dict + A dictionary where the keys represent new attribute names (strings), and the values can be: + - Scalars: int, float, or str. + - Compound values (dictionaries) for more complex metadata, which are converted to NumPy structured arrays. + Example of a compound value: + + Example: + ---------- + annotation_dict = { + "relative_humidity": { + "value": 65, + "units": "percentage", + "range": "[0,100]", + "definition": "amount of water vapor present ..." + } + } + """ + + if self.file_obj is None: + raise RuntimeError("File object is not loaded. Please load the HDF5 file using the 'load_file_obj' method before attempting to modify it.") + + # Create a copy of annotation_dict to avoid modifying the original + annotation_dict_copy = copy.deepcopy(annotation_dict) + + try: + obj = self.file_obj[obj_name] + + # Check if any attribute already exists + if any(key in obj.attrs for key in annotation_dict_copy.keys()): + raise ValueError("Make sure the provided (key, value) pairs are not existing metadata elements or attributes. To modify or delete existing attributes use .modify_annotation() or .delete_annotation()") + + # Process the dictionary values and convert them to structured arrays if needed + for key, value in annotation_dict_copy.items(): + if isinstance(value, dict): + # Convert dictionaries to NumPy structured arrays for complex attributes + annotation_dict_copy[key] = utils.convert_attrdict_to_np_structured_array(value) + + # Update the object's attributes with the new metadata + obj.attrs.update(annotation_dict_copy) + + except Exception as e: + self.unload_file_obj() + print(f"An unexpected error occurred: {e}. The file object has been properly closed.")
+ + + +
+[docs] + def update_metadata(self, obj_name, annotation_dict): + """ + Updates the value of existing metadata attributes of the specified object (obj_name) based on the provided annotation_dict. + + The function disregards non-existing attributes and suggests to use the append_metadata() method to include those in the metadata. + + Parameters: + ----------- + obj_name : str + Path to the target object (dataset or group) within the HDF5 file. + + annotation_dict: dict + A dictionary where the keys represent existing attribute names (strings), and the values can be: + - Scalars: int, float, or str. + - Compound values (dictionaries) for more complex metadata, which are converted to NumPy structured arrays. + Example of a compound value: + + Example: + ---------- + annotation_dict = { + "relative_humidity": { + "value": 65, + "units": "percentage", + "range": "[0,100]", + "definition": "amount of water vapor present ..." + } + } + + + """ + + if self.file_obj is None: + raise RuntimeError("File object is not loaded. Please load the HDF5 file using the 'load_file_obj' method before attempting to modify it.") + + update_dict = {} + + try: + + obj = self.file_obj[obj_name] + for key, value in annotation_dict.items(): + if key in obj.attrs: + if isinstance(value, dict): + update_dict[key] = utils.convert_attrdict_to_np_structured_array(value) + else: + update_dict[key] = value + else: + # Optionally, log or warn about non-existing keys being ignored. + print(f"Warning: Key '{key}' does not exist and will be ignored.") + + obj.attrs.update(update_dict) + + except Exception as e: + self.unload_file_obj() + print(f"An unexpected error occurred: {e}. The file object has been properly closed.")
+ + +
+[docs] + def delete_metadata(self, obj_name, annotation_dict): + """ + Deletes metadata attributes of the specified object (obj_name) based on the provided annotation_dict. + + Parameters: + ----------- + obj_name: str + Path to the target object (dataset or group) within the HDF5 file. + + annotation_dict: dict + Dictionary where keys represent attribute names, and values should be dictionaries containing + {"delete": True} to mark them for deletion. + + Example: + -------- + annotation_dict = {"attr_to_be_deleted": {"delete": True}} + + Behavior: + --------- + - Deletes the specified attributes from the object's metadata if marked for deletion. + - Issues a warning if the attribute is not found or not marked for deletion. + """ + + if self.file_obj is None: + raise RuntimeError("File object is not loaded. Please load the HDF5 file using the 'load_file_obj' method before attempting to modify it.") + + try: + obj = self.file_obj[obj_name] + for attr_key, value in annotation_dict.items(): + if attr_key in obj.attrs: + if isinstance(value, dict) and value.get('delete', False): + obj.attrs.__delitem__(attr_key) + else: + msg = f"Warning: Value for key '{attr_key}' is not marked for deletion or is invalid." + print(msg) + else: + msg = f"Warning: Key '{attr_key}' does not exist in metadata." + print(msg) + + except Exception as e: + self.unload_file_obj() + print(f"An unexpected error occurred: {e}. The file object has been properly closed.")
+ + + +
+[docs] + def rename_metadata(self, obj_name, renaming_map): + """ + Renames metadata attributes of the specified object (obj_name) based on the provided renaming_map. + + Parameters: + ----------- + obj_name: str + Path to the target object (dataset or group) within the HDF5 file. + + renaming_map: dict + A dictionary where keys are current attribute names (strings), and values are the new attribute names (strings or byte strings) to rename to. + + Example: + -------- + renaming_map = { + "old_attr_name": "new_attr_name", + "old_attr_2": "new_attr_2" + } + + """ + + if self.file_obj is None: + raise RuntimeError("File object is not loaded. Please load the HDF5 file using the 'load_file_obj' method before attempting to modify it.") + + try: + obj = self.file_obj[obj_name] + # Iterate over the renaming_map to process renaming + for old_attr, new_attr in renaming_map.items(): + if old_attr in obj.attrs: + # Get the old attribute's value + attr_value = obj.attrs[old_attr] + + # Create a new attribute with the new name + obj.attrs.create(new_attr, data=attr_value) + + # Delete the old attribute + obj.attrs.__delitem__(old_attr) + else: + # Skip if the old attribute doesn't exist + msg = f"Skipping: Attribute '{old_attr}' does not exist." + print(msg) # Optionally, replace with warnings.warn(msg) + except Exception as e: + self.unload_file_obj() + print( + f"An unexpected error occurred: {e}. The file object has been properly closed. " + "Please ensure that 'obj_name' exists in the file, and that the keys in 'renaming_map' are valid attributes of the object." + ) + + self.unload_file_obj()
+ + +
+[docs] + def get_metadata(self, obj_path): + """ Get file attributes from object at path = obj_path. For example, + obj_path = '/' will get root level attributes or metadata. + """ + try: + # Access the attributes for the object at the given path + metadata_dict = self.file_obj[obj_path].attrs + except KeyError: + # Handle the case where the path doesn't exist + logging.error(f'Invalid object path: {obj_path}') + metadata_dict = {} + + return metadata_dict
+ + + +
+[docs] + def reformat_datetime_column(self, dataset_name, column_name, src_format, desired_format='%Y-%m-%d %H:%M:%S.%f'): + # Access the dataset + dataset = self.file_obj[dataset_name] + + # Read the column data into a pandas Series and decode bytes to strings + dt_column_data = pd.Series(dataset[column_name][:]).apply(lambda x: x.decode() ) + + # Convert to datetime using the source format + dt_column_data = pd.to_datetime(dt_column_data, format=src_format, errors = 'coerce') + + # Reformat datetime objects to the desired format as strings + dt_column_data = dt_column_data.dt.strftime(desired_format) + + # Encode the strings back to bytes + #encoded_data = dt_column_data.apply(lambda x: x.encode() if not pd.isnull(x) else 'N/A').to_numpy() + + # Update the dataset in place + #dataset[column_name][:] = encoded_data + + # Convert byte strings to datetime objects + #timestamps = [datetime.datetime.strptime(a.decode(), src_format).strftime(desired_format) for a in dt_column_data] + + #datetime.strptime('31/01/22 23:59:59.999999', + # '%d/%m/%y %H:%M:%S.%f') + + #pd.to_datetime( + # np.array([a.decode() for a in dt_column_data]), + # format=src_format, + # errors='coerce' + #) + + + # Standardize the datetime format + #standardized_time = datetime.strftime(desired_format) + + # Convert to byte strings to store back in the HDF5 dataset + #standardized_time_bytes = np.array([s.encode() for s in timestamps]) + + # Update the column in the dataset (in-place update) + # TODO: make this a more secure operation + #dataset[column_name][:] = standardized_time_bytes + + #return np.array(timestamps) + return dt_column_data.to_numpy()
+ + + # Define data append operations: append_dataset(), and update_file() + +
+[docs] + def append_dataset(self,dataset_dict, group_name): + + # Parse value into HDF5 admissible type + for key in dataset_dict['attributes'].keys(): + value = dataset_dict['attributes'][key] + if isinstance(value, dict): + dataset_dict['attributes'][key] = utils.convert_attrdict_to_np_structured_array(value) + + if not group_name in self.file_obj: + self.file_obj.create_group(group_name, track_order=True) + self.file_obj[group_name].attrs['creation_date'] = utils.created_at().encode("utf-8") + + self.file_obj[group_name].create_dataset(dataset_dict['name'], data=dataset_dict['data']) + self.file_obj[group_name][dataset_dict['name']].attrs.update(dataset_dict['attributes']) + self.file_obj[group_name].attrs['last_update_date'] = utils.created_at().encode("utf-8")
+ + +
+[docs] + def update_file(self, path_to_append_dir): + # Split the reference file path and the append directory path into directories and filenames + ref_tail, ref_head = os.path.split(self.file_path) + ref_head_filename, head_ext = os.path.splitext(ref_head) + tail, head = os.path.split(path_to_append_dir) + + + # Ensure the append directory is in the same directory as the reference file and has the same name (without extension) + if not (ref_tail == tail and ref_head_filename == head): + raise ValueError("The append directory must be in the same directory as the reference HDF5 file and have the same name without the extension.") + + # Close the file if it's already open + if self.file_obj is not None: + self.unload_file_obj() + + # Attempt to open the file in 'r+' mode for appending + try: + hdf5_lib.create_hdf5_file_from_filesystem_path(path_to_append_dir, mode='r+') + except FileNotFoundError: + raise FileNotFoundError(f"Reference HDF5 file '{self.file_path}' not found.") + except OSError as e: + raise OSError(f"Error opening HDF5 file: {e}")
+
+ + + + +
+[docs] +def get_parent_child_relationships(file: h5py.File): + + nodes = ['/'] + parent = [''] + #values = [file.attrs['count']] + # TODO: maybe we should make this more general and not dependent on file_list attribute? + #if 'file_list' in file.attrs.keys(): + # values = [len(file.attrs['file_list'])] + #else: + # values = [1] + values = [len(file.keys())] + + def node_visitor(name,obj): + if name.count('/') <=2: + nodes.append(obj.name) + parent.append(obj.parent.name) + #nodes.append(os.path.split(obj.name)[1]) + #parent.append(os.path.split(obj.parent.name)[1]) + + if isinstance(obj,h5py.Dataset):# or not 'file_list' in obj.attrs.keys(): + values.append(1) + else: + print(obj.name) + try: + values.append(len(obj.keys())) + except: + values.append(0) + + file.visititems(node_visitor) + + return nodes, parent, values
+ + + +def __print_metadata__(name, obj, folder_depth, yaml_dict): + + """ + Extracts metadata from HDF5 groups and datasets and organizes them into a dictionary with compact representation. + + Parameters: + ----------- + name (str): Name of the HDF5 object being inspected. + obj (h5py.Group or h5py.Dataset): The HDF5 object (Group or Dataset). + folder_depth (int): Maximum depth of folders to explore. + yaml_dict (dict): Dictionary to populate with metadata. + """ + # Process only objects within the specified folder depth + if len(obj.name.split('/')) <= folder_depth: # and ".h5" not in obj.name: + name_to_list = obj.name.split('/') + name_head = name_to_list[-1] if not name_to_list[-1]=='' else obj.name + + if isinstance(obj, h5py.Group): # Handle groups + # Convert attributes to a YAML/JSON serializable format + attr_dict = {key: utils.to_serializable_dtype(val) for key, val in obj.attrs.items()} + + # Initialize the group dictionary + group_dict = {"name": name_head, "attributes": attr_dict} + + # Handle group members compactly + #subgroups = [member_name for member_name in obj if isinstance(obj[member_name], h5py.Group)] + #datasets = [member_name for member_name in obj if isinstance(obj[member_name], h5py.Dataset)] + + # Summarize groups and datasets + #group_dict["content_summary"] = { + # "group_count": len(subgroups), + # "group_preview": subgroups[:3] + (["..."] if len(subgroups) > 3 else []), + # "dataset_count": len(datasets), + # "dataset_preview": datasets[:3] + (["..."] if len(datasets) > 3 else []) + #} + + yaml_dict[obj.name] = group_dict + + elif isinstance(obj, h5py.Dataset): # Handle datasets + # Convert attributes to a YAML/JSON serializable format + attr_dict = {key: utils.to_serializable_dtype(val) for key, val in obj.attrs.items()} + + dataset_dict = {"name": name_head, "attributes": attr_dict} + + yaml_dict[obj.name] = dataset_dict + + + +
+[docs] +def serialize_metadata(input_filename_path, folder_depth: int = 4, output_format: str = 'yaml') -> str: + """ + Serialize metadata from an HDF5 file into YAML or JSON format. + + Parameters + ---------- + input_filename_path : str + The path to the input HDF5 file. + folder_depth : int, optional + The folder depth to control how much of the HDF5 file hierarchy is traversed (default is 4). + output_format : str, optional + The format to serialize the output, either 'yaml' or 'json' (default is 'yaml'). + + Returns + ------- + str + The output file path where the serialized metadata is stored (either .yaml or .json). + + """ + + # Choose the appropriate output format (YAML or JSON) + if output_format not in ['yaml', 'json']: + raise ValueError("Unsupported format. Please choose either 'yaml' or 'json'.") + + # Initialize dictionary to store YAML/JSON data + yaml_dict = {} + + # Split input file path to get the output file's base name + output_filename_tail, ext = os.path.splitext(input_filename_path) + + # Open the HDF5 file and extract metadata + with h5py.File(input_filename_path, 'r') as f: + # Convert attribute dict to a YAML/JSON serializable dict + #attrs_dict = {key: utils.to_serializable_dtype(val) for key, val in f.attrs.items()} + #yaml_dict[f.name] = { + # "name": f.name, + # "attributes": attrs_dict, + # "datasets": {} + #} + __print_metadata__(f.name, f, folder_depth, yaml_dict) + # Traverse HDF5 file hierarchy and add datasets + f.visititems(lambda name, obj: __print_metadata__(name, obj, folder_depth, yaml_dict)) + + + # Serialize and write the data + output_file_path = output_filename_tail + '.' + output_format + with open(output_file_path, 'w') as output_file: + if output_format == 'json': + json_output = json.dumps(yaml_dict, indent=4, sort_keys=False) + output_file.write(json_output) + elif output_format == 'yaml': + yaml_output = yaml.dump(yaml_dict, sort_keys=False) + output_file.write(yaml_output) + + return output_file_path
+ + + +
+[docs] +def get_groups_at_a_level(file: h5py.File, level: str): + + groups = [] + def node_selector(name, obj): + if name.count('/') == level: + print(name) + groups.append(obj.name) + + file.visititems(node_selector) + #file.visititems() + return groups
+ + +
+[docs] +def read_mtable_as_dataframe(filename): + + """ + Reconstruct a MATLAB Table encoded in a .h5 file as a Pandas DataFrame. + + This function reads a .h5 file containing a MATLAB Table and reconstructs it as a Pandas DataFrame. + The input .h5 file contains one group per row of the MATLAB Table. Each group stores the table's + dataset-like variables as Datasets, while categorical and numerical variables are represented as + attributes of the respective group. + + To ensure homogeneity of data columns, the DataFrame is constructed column-wise. + + Parameters + ---------- + filename : str + The name of the .h5 file. This may include the file's location and path information. + + Returns + ------- + pd.DataFrame + The MATLAB Table reconstructed as a Pandas DataFrame. + """ + + + #contructs dataframe by filling out entries columnwise. This way we can ensure homogenous data columns""" + + with h5py.File(filename,'r') as file: + + # Define group's attributes and datasets. This should hold + # for all groups. TODO: implement verification and noncompliance error if needed. + group_list = list(file.keys()) + group_attrs = list(file[group_list[0]].attrs.keys()) + # + column_attr_names = [item[item.find('_')+1::] for item in group_attrs] + column_attr_names_idx = [int(item[4:(item.find('_'))]) for item in group_attrs] + + group_datasets = list(file[group_list[0]].keys()) if not 'DS_EMPTY' in file[group_list[0]].keys() else [] + # + column_dataset_names = [file[group_list[0]][item].attrs['column_name'] for item in group_datasets] + column_dataset_names_idx = [int(item[2:]) for item in group_datasets] + + + # Define data_frame as group_attrs + group_datasets + #pd_series_index = group_attrs + group_datasets + pd_series_index = column_attr_names + column_dataset_names + + output_dataframe = pd.DataFrame(columns=pd_series_index,index=group_list) + + tmp_col = [] + + for meas_prop in group_attrs + group_datasets: + if meas_prop in group_attrs: + column_label = meas_prop[meas_prop.find('_')+1:] + # Create numerical or categorical column from group's attributes + tmp_col = [file[group_key].attrs[meas_prop][()][0] for group_key in group_list] + else: + # Create dataset column from group's datasets + column_label = file[group_list[0] + '/' + meas_prop].attrs['column_name'] + #tmp_col = [file[group_key + '/' + meas_prop][()][0] for group_key in group_list] + tmp_col = [file[group_key + '/' + meas_prop][()] for group_key in group_list] + + output_dataframe.loc[:,column_label] = tmp_col + + return output_dataframe
+ + +if __name__ == "__main__": + if len(sys.argv) < 5: + print("Usage: python hdf5_ops.py serialize <path/to/target_file.hdf5> <folder_depth : int = 2> <format=json|yaml>") + sys.exit(1) + + if sys.argv[1] == 'serialize': + input_hdf5_file = sys.argv[2] + folder_depth = int(sys.argv[3]) + file_format = sys.argv[4] + + try: + # Call the serialize_metadata function and capture the output path + path_to_file = serialize_metadata(input_hdf5_file, + folder_depth = folder_depth, + output_format=file_format) + print(f"Metadata serialized to {path_to_file}") + except Exception as e: + print(f"An error occurred during serialization: {e}") + sys.exit(1) + + #run(sys.argv[2]) + +
+ +
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/docs/build/html/_modules/src/hdf5_vis.html b/docs/build/html/_modules/src/hdf5_vis.html index 83e2160..7c000e4 100644 --- a/docs/build/html/_modules/src/hdf5_vis.html +++ b/docs/build/html/_modules/src/hdf5_vis.html @@ -1,180 +1,180 @@ - - - - - - src.hdf5_vis — DIMA 1.0.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for src.hdf5_vis

-import sys
-import os
-root_dir = os.path.abspath(os.curdir)
-sys.path.append(root_dir)
-
-import h5py
-import yaml
-
-import numpy as np
-import pandas as pd
-
-from plotly.subplots import make_subplots
-import plotly.graph_objects as go
-import plotly.express as px
-#import plotly.io as pio
-from src.hdf5_ops import get_parent_child_relationships
-
- 
-
-
-[docs] -def display_group_hierarchy_on_a_treemap(filename: str): - - """ - filename (str): hdf5 file's filename""" - - with h5py.File(filename,'r') as file: - nodes, parents, values = get_parent_child_relationships(file) - - metadata_list = [] - metadata_dict={} - for key in file.attrs.keys(): - #if 'metadata' in key: - if isinstance(file.attrs[key], str): # Check if the attribute is a string - metadata_key = key[key.find('_') + 1:] - metadata_value = file.attrs[key] - metadata_dict[metadata_key] = metadata_value - metadata_list.append(f'{metadata_key}: {metadata_value}') - - #metadata_dict[key[key.find('_')+1::]]= file.attrs[key] - #metadata_list.append(key[key.find('_')+1::]+':'+file.attrs[key]) - - metadata = '<br>'.join(['<br>'] + metadata_list) - - customdata_series = pd.Series(nodes) - customdata_series[0] = metadata - - fig = make_subplots(1, 1, specs=[[{"type": "domain"}]],) - fig.add_trace(go.Treemap( - labels=nodes, #formating_df['formated_names'][nodes], - parents=parents,#formating_df['formated_names'][parents], - values=values, - branchvalues='remainder', - customdata= customdata_series, - #marker=dict( - # colors=df_all_trees['color'], - # colorscale='RdBu', - # cmid=average_score), - #hovertemplate='<b>%{label} </b> <br> Number of files: %{value}<br> Success rate: %{color:.2f}', - hovertemplate='<b>%{label} </b> <br> Count: %{value} <br> Path: %{customdata}', - name='', - root_color="lightgrey" - )) - fig.update_layout(width = 800, height= 600, margin = dict(t=50, l=25, r=25, b=25)) - fig.show() - file_name, file_ext = os.path.splitext(filename) - fig.write_html(file_name + ".html")
- - - #pio.write_image(fig,file_name + ".png",width=800,height=600,format='png') - -# -
- -
-
- -
-
-
-
- - - + + + + + + src.hdf5_vis — DIMA 1.0.0 documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for src.hdf5_vis

+import sys
+import os
+root_dir = os.path.abspath(os.curdir)
+sys.path.append(root_dir)
+
+import h5py
+import yaml
+
+import numpy as np
+import pandas as pd
+
+from plotly.subplots import make_subplots
+import plotly.graph_objects as go
+import plotly.express as px
+#import plotly.io as pio
+from src.hdf5_ops import get_parent_child_relationships
+
+ 
+
+
+[docs] +def display_group_hierarchy_on_a_treemap(filename: str): + + """ + filename (str): hdf5 file's filename""" + + with h5py.File(filename,'r') as file: + nodes, parents, values = get_parent_child_relationships(file) + + metadata_list = [] + metadata_dict={} + for key in file.attrs.keys(): + #if 'metadata' in key: + if isinstance(file.attrs[key], str): # Check if the attribute is a string + metadata_key = key[key.find('_') + 1:] + metadata_value = file.attrs[key] + metadata_dict[metadata_key] = metadata_value + metadata_list.append(f'{metadata_key}: {metadata_value}') + + #metadata_dict[key[key.find('_')+1::]]= file.attrs[key] + #metadata_list.append(key[key.find('_')+1::]+':'+file.attrs[key]) + + metadata = '<br>'.join(['<br>'] + metadata_list) + + customdata_series = pd.Series(nodes) + customdata_series[0] = metadata + + fig = make_subplots(1, 1, specs=[[{"type": "domain"}]],) + fig.add_trace(go.Treemap( + labels=nodes, #formating_df['formated_names'][nodes], + parents=parents,#formating_df['formated_names'][parents], + values=values, + branchvalues='remainder', + customdata= customdata_series, + #marker=dict( + # colors=df_all_trees['color'], + # colorscale='RdBu', + # cmid=average_score), + #hovertemplate='<b>%{label} </b> <br> Number of files: %{value}<br> Success rate: %{color:.2f}', + hovertemplate='<b>%{label} </b> <br> Count: %{value} <br> Path: %{customdata}', + name='', + root_color="lightgrey" + )) + fig.update_layout(width = 800, height= 600, margin = dict(t=50, l=25, r=25, b=25)) + fig.show() + file_name, file_ext = os.path.splitext(filename) + fig.write_html(file_name + ".html")
+ + + #pio.write_image(fig,file_name + ".png",width=800,height=600,format='png') + +# +
+ +
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/docs/build/html/_modules/src/hdf5_writer.html b/docs/build/html/_modules/src/hdf5_writer.html index 3a54d8a..aaffe03 100644 --- a/docs/build/html/_modules/src/hdf5_writer.html +++ b/docs/build/html/_modules/src/hdf5_writer.html @@ -1,513 +1,513 @@ - - - - - - src.hdf5_writer — DIMA 1.0.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for src.hdf5_writer

-import sys
-import os
-root_dir = os.path.abspath(os.curdir)
-sys.path.append(root_dir)
-
-import pandas as pd
-import numpy as np
-import h5py
-import logging
-
-import utils.g5505_utils as utils
-import instruments.readers.filereader_registry as filereader_registry
-
- 
-   
-def __transfer_file_dict_to_hdf5(h5file, group_name, file_dict):
-    """
-    Transfers data from a file_dict to an HDF5 file.
-
-    Parameters
-    ----------
-    h5file : h5py.File
-        HDF5 file object where the data will be written.
-    group_name : str
-        Name of the HDF5 group where data will be stored.
-    file_dict : dict
-        Dictionary containing file data to be transferred. Required structure:
-        {
-            'name': str,
-            'attributes_dict': dict,
-            'datasets': [
-                {
-                    'name': str,
-                    'data': array-like,
-                    'shape': tuple,
-                    'attributes': dict (optional)
-                },
-                ...
-            ]
-        }
-
-    Returns
-    -------
-    None
-    """
-
-    if not file_dict:
-        return
-
-    try:
-        # Create group and add their attributes
-        filename = file_dict['name']
-        group = h5file[group_name].create_group(name=filename)
-        # Add group attributes                                
-        group.attrs.update(file_dict['attributes_dict'])
-        
-        # Add datasets to the just created group
-        for dataset in file_dict['datasets']:
-            dataset_obj = group.create_dataset(
-                name=dataset['name'], 
-                data=dataset['data'],
-                shape=dataset['shape']
-            )
-            
-            # Add dataset's attributes                                
-            attributes = dataset.get('attributes', {})
-            dataset_obj.attrs.update(attributes)
-        group.attrs['last_update_date'] = utils.created_at().encode('utf-8')
-
-        stdout = f'Completed transfer for /{group_name}/{filename}'
-
-    except Exception as inst: 
-        stdout = inst
-        logging.error('Failed to transfer data into HDF5: %s', inst)
-
-    return stdout
-
-def __copy_file_in_group(source_file_path, dest_file_obj : h5py.File, dest_group_name, work_with_copy : bool = True):
-    # Create copy of original file to avoid possible file corruption and work with it.
-
-    if work_with_copy:
-        tmp_file_path = utils.make_file_copy(source_file_path)
-    else:
-        tmp_file_path = source_file_path
-
-    # Open backup h5 file and copy complet filesystem directory onto a group in h5file
-    with h5py.File(tmp_file_path,'r') as src_file:
-        dest_file_obj.copy(source= src_file['/'], dest= dest_group_name)
-
-    if 'tmp_files' in tmp_file_path:
-        os.remove(tmp_file_path)
-
-    stdout = f'Completed transfer for /{dest_group_name}'
-    return stdout
-
-
-[docs] -def create_hdf5_file_from_filesystem_path(path_to_input_directory: str, - path_to_filenames_dict: dict = None, - select_dir_keywords : list = [], - root_metadata_dict : dict = {}, mode = 'w'): - - """ - Creates an .h5 file with name "output_filename" that preserves the directory tree (or folder structure) - of a given filesystem path. - - The data integration capabilities are limited by our file reader, which can only access data from a list of - admissible file formats. These, however, can be extended. Directories are groups in the resulting HDF5 file. - Files are formatted as composite objects consisting of a group, file, and attributes. - - Parameters - ---------- - output_filename : str - Name of the output HDF5 file. - path_to_input_directory : str - Path to root directory, specified with forward slashes, e.g., path/to/root. - - path_to_filenames_dict : dict, optional - A pre-processed dictionary where keys are directory paths on the input directory's tree and values are lists of files. - If provided, 'input_file_system_path' is ignored. - - select_dir_keywords : list - List of string elements to consider or select only directory paths that contain - a word in 'select_dir_keywords'. When empty, all directory paths are considered - to be included in the HDF5 file group hierarchy. - root_metadata_dict : dict - Metadata to include at the root level of the HDF5 file. - - mode : str - 'w' create File, truncate if it exists, or 'r+' read/write, File must exists. By default, mode = "w". - - Returns - ------- - output_filename : str - Path to the created HDF5 file. - """ - - - if not mode in ['w','r+']: - raise ValueError(f'Parameter mode must take values in ["w","r+"]') - - if not '/' in path_to_input_directory: - raise ValueError('path_to_input_directory needs to be specified using forward slashes "/".' ) - - #path_to_output_directory = os.path.join(path_to_input_directory,'..') - path_to_input_directory = os.path.normpath(path_to_input_directory).rstrip(os.sep) - - - for i, keyword in enumerate(select_dir_keywords): - select_dir_keywords[i] = keyword.replace('/',os.sep) - - if not path_to_filenames_dict: - # On dry_run=True, returns path to files dictionary of the output directory without making a actual copy of the input directory. - # Therefore, there wont be a copying conflict by setting up input and output directories the same - path_to_filenames_dict = utils.copy_directory_with_contraints(input_dir_path=path_to_input_directory, - output_dir_path=path_to_input_directory, - dry_run=True) - # Set input_directory as copied input directory - root_dir = path_to_input_directory - path_to_output_file = path_to_input_directory.rstrip(os.path.sep) + '.h5' - - start_message = f'\n[Start] Data integration :\nSource: {path_to_input_directory}\nDestination: {path_to_output_file}\n' - - print(start_message) - logging.info(start_message) - - # Check if the .h5 file already exists - if os.path.exists(path_to_output_file) and mode in ['w']: - message = ( - f"[Notice] The file '{path_to_output_file}' already exists and will not be overwritten.\n" - "If you wish to replace it, please delete the existing file first and rerun the program." - ) - print(message) - logging.error(message) - else: - with h5py.File(path_to_output_file, mode=mode, track_order=True) as h5file: - - number_of_dirs = len(path_to_filenames_dict.keys()) - dir_number = 1 - for dirpath, filtered_filenames_list in path_to_filenames_dict.items(): - - # Check if filtered_filenames_list is nonempty. TODO: This is perhaps redundant by design of path_to_filenames_dict. - if not filtered_filenames_list: - continue - - group_name = dirpath.replace(os.sep,'/') - group_name = group_name.replace(root_dir.replace(os.sep,'/') + '/', '/') - - # Flatten group name to one level - if select_dir_keywords: - offset = sum([len(i.split(os.sep)) if i in dirpath else 0 for i in select_dir_keywords]) - else: - offset = 1 - tmp_list = group_name.split('/') - if len(tmp_list) > offset+1: - group_name = '/'.join([tmp_list[i] for i in range(offset+1)]) - - # Create group called "group_name". Hierarchy of nested groups can be implicitly defined by the forward slashes - if not group_name in h5file.keys(): - h5file.create_group(group_name) - h5file[group_name].attrs['creation_date'] = utils.created_at().encode('utf-8') - #h5file[group_name].attrs.create(name='filtered_file_list',data=convert_string_to_bytes(filtered_filename_list)) - #h5file[group_name].attrs.create(name='file_list',data=convert_string_to_bytes(filenames_list)) - #else: - #print(group_name,' was already created.') - instFoldermsgStart = f'Starting data transfer from instFolder: {group_name}' - print(instFoldermsgStart) - - for filenumber, filename in enumerate(filtered_filenames_list): - - #file_ext = os.path.splitext(filename)[1] - #try: - - # hdf5 path to filename group - dest_group_name = f'{group_name}/{filename}' - - if not 'h5' in filename: - #file_dict = config_file.select_file_readers(group_id)[file_ext](os.path.join(dirpath,filename)) - #file_dict = ext_to_reader_dict[file_ext](os.path.join(dirpath,filename)) - file_dict = filereader_registry.select_file_reader(dest_group_name)(os.path.join(dirpath,filename)) - - stdout = __transfer_file_dict_to_hdf5(h5file, group_name, file_dict) - - else: - source_file_path = os.path.join(dirpath,filename) - dest_file_obj = h5file - #group_name +'/'+filename - #ext_to_reader_dict[file_ext](source_file_path, dest_file_obj, dest_group_name) - #g5505f_reader.select_file_reader(dest_group_name)(source_file_path, dest_file_obj, dest_group_name) - stdout = __copy_file_in_group(source_file_path, dest_file_obj, dest_group_name, False) - - # Update the progress bar and log the end message - instFoldermsdEnd = f'\nCompleted data transfer for instFolder: {group_name}\n' - # Print and log the start message - utils.progressBar(dir_number, number_of_dirs, instFoldermsdEnd) - logging.info(instFoldermsdEnd ) - dir_number = dir_number + 1 - - print('[End] Data integration') - logging.info('[End] Data integration') - - if len(root_metadata_dict.keys())>0: - for key, value in root_metadata_dict.items(): - #if key in h5file.attrs: - # del h5file.attrs[key] - h5file.attrs.create(key, value) - #annotate_root_dir(output_filename,root_metadata_dict) - - - #output_yml_filename_path = hdf5_vis.take_yml_snapshot_of_hdf5_file(output_filename) - - return path_to_output_file #, output_yml_filename_path
- - -
-[docs] -def create_hdf5_file_from_dataframe(ofilename, input_data, group_by_funcs: list, approach: str = None, extract_attrs_func=None): - """ - Creates an HDF5 file with hierarchical groups based on the specified grouping functions or columns. - - Parameters: - ----------- - ofilename (str): Path for the output HDF5 file. - input_data (pd.DataFrame or str): Input data as a DataFrame or a valid file system path. - group_by_funcs (list): List of callables or column names to define hierarchical grouping. - approach (str): Specifies the approach ('top-down' or 'bottom-up') for creating the HDF5 file. - extract_attrs_func (callable, optional): Function to extract additional attributes for HDF5 groups. - - Returns: - -------- - None - """ - # Check whether input_data is a valid file-system path or a DataFrame - is_valid_path = lambda x: os.path.exists(x) if isinstance(x, str) else False - - if is_valid_path(input_data): - # If input_data is a file-system path, create a DataFrame with file info - file_list = os.listdir(input_data) - df = pd.DataFrame(file_list, columns=['filename']) - df = utils.augment_with_filetype(df) # Add filetype information if needed - elif isinstance(input_data, pd.DataFrame): - # If input_data is a DataFrame, make a copy - df = input_data.copy() - else: - raise ValueError("input_data must be either a valid file-system path or a DataFrame.") - - # Generate grouping columns based on group_by_funcs - if utils.is_callable_list(group_by_funcs): - grouping_cols = [] - for i, func in enumerate(group_by_funcs): - col_name = f'level_{i}_groups' - grouping_cols.append(col_name) - df[col_name] = func(df) - elif utils.is_str_list(group_by_funcs) and all([item in df.columns for item in group_by_funcs]): - grouping_cols = group_by_funcs - else: - raise ValueError("'group_by_funcs' must be a list of callables or valid column names in the DataFrame.") - - # Generate group paths - df['group_path'] = ['/' + '/'.join(row) for row in df[grouping_cols].values.astype(str)] - - # Open the HDF5 file in write mode - with h5py.File(ofilename, 'w') as file: - for group_path in df['group_path'].unique(): - # Create groups in HDF5 - group = file.create_group(group_path) - - # Filter the DataFrame for the current group - datatable = df[df['group_path'] == group_path].copy() - - # Drop grouping columns and the generated 'group_path' - datatable = datatable.drop(columns=grouping_cols + ['group_path']) - - # Add datasets to groups if data exists - if not datatable.empty: - dataset = utils.convert_dataframe_to_np_structured_array(datatable) - group.create_dataset(name='data_table', data=dataset) - - # Add attributes if extract_attrs_func is provided - if extract_attrs_func: - attrs = extract_attrs_func(datatable) - for key, value in attrs.items(): - group.attrs[key] = value - - # Save metadata about depth of hierarchy - file.attrs.create(name='depth', data=len(grouping_cols) - 1) - - print(f"HDF5 file created successfully at {ofilename}") - - return ofilename
- - - -
-[docs] -def save_processed_dataframe_to_hdf5(df, annotator, output_filename): # src_hdf5_path, script_date, script_name): - """ - Save processed dataframe columns with annotations to an HDF5 file. - - Parameters: - df (pd.DataFrame): DataFrame containing processed time series. - annotator (): Annotator object with get_metadata method. - output_filename (str): Path to the source HDF5 file. - """ - # Convert datetime columns to string - datetime_cols = df.select_dtypes(include=['datetime64']).columns - - if list(datetime_cols): - df[datetime_cols] = df[datetime_cols].map(str) - - # Convert dataframe to structured array - icad_data_table = utils.convert_dataframe_to_np_structured_array(df) - - # Get metadata - metadata_dict = annotator.get_metadata() - - # Prepare project level attributes to be added at the root level - - project_level_attributes = metadata_dict['metadata']['project'] - - # Prepare high-level attributes - high_level_attributes = { - 'parent_files': metadata_dict['parent_files'], - **metadata_dict['metadata']['sample'], - **metadata_dict['metadata']['environment'], - **metadata_dict['metadata']['instruments'] - } - - # Prepare data level attributes - data_level_attributes = metadata_dict['metadata']['datasets'] - - for key, value in data_level_attributes.items(): - if isinstance(value,dict): - data_level_attributes[key] = utils.convert_attrdict_to_np_structured_array(value) - - - # Prepare file dictionary - file_dict = { - 'name': project_level_attributes['processing_file'], - 'attributes_dict': high_level_attributes, - 'datasets': [{ - 'name': "data_table", - 'data': icad_data_table, - 'shape': icad_data_table.shape, - 'attributes': data_level_attributes - }] - } - - # Check if the file exists - if os.path.exists(output_filename): - mode = "a" - print(f"File {output_filename} exists. Opening in append mode.") - else: - mode = "w" - print(f"File {output_filename} does not exist. Creating a new file.") - - - # Write to HDF5 - with h5py.File(output_filename, mode) as h5file: - # Add project level attributes at the root/top level - h5file.attrs.update(project_level_attributes) - __transfer_file_dict_to_hdf5(h5file, '/', file_dict)
- - -#if __name__ == '__main__': -
- -
-
- -
-
-
-
- - - + + + + + + src.hdf5_writer — DIMA 1.0.0 documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for src.hdf5_writer

+import sys
+import os
+root_dir = os.path.abspath(os.curdir)
+sys.path.append(root_dir)
+
+import pandas as pd
+import numpy as np
+import h5py
+import logging
+
+import utils.g5505_utils as utils
+import instruments.readers.filereader_registry as filereader_registry
+
+ 
+   
+def __transfer_file_dict_to_hdf5(h5file, group_name, file_dict):
+    """
+    Transfers data from a file_dict to an HDF5 file.
+
+    Parameters
+    ----------
+    h5file : h5py.File
+        HDF5 file object where the data will be written.
+    group_name : str
+        Name of the HDF5 group where data will be stored.
+    file_dict : dict
+        Dictionary containing file data to be transferred. Required structure:
+        {
+            'name': str,
+            'attributes_dict': dict,
+            'datasets': [
+                {
+                    'name': str,
+                    'data': array-like,
+                    'shape': tuple,
+                    'attributes': dict (optional)
+                },
+                ...
+            ]
+        }
+
+    Returns
+    -------
+    None
+    """
+
+    if not file_dict:
+        return
+
+    try:
+        # Create group and add their attributes
+        filename = file_dict['name']
+        group = h5file[group_name].create_group(name=filename)
+        # Add group attributes                                
+        group.attrs.update(file_dict['attributes_dict'])
+        
+        # Add datasets to the just created group
+        for dataset in file_dict['datasets']:
+            dataset_obj = group.create_dataset(
+                name=dataset['name'], 
+                data=dataset['data'],
+                shape=dataset['shape']
+            )
+            
+            # Add dataset's attributes                                
+            attributes = dataset.get('attributes', {})
+            dataset_obj.attrs.update(attributes)
+        group.attrs['last_update_date'] = utils.created_at().encode('utf-8')
+
+        stdout = f'Completed transfer for /{group_name}/{filename}'
+
+    except Exception as inst: 
+        stdout = inst
+        logging.error('Failed to transfer data into HDF5: %s', inst)
+
+    return stdout
+
+def __copy_file_in_group(source_file_path, dest_file_obj : h5py.File, dest_group_name, work_with_copy : bool = True):
+    # Create copy of original file to avoid possible file corruption and work with it.
+
+    if work_with_copy:
+        tmp_file_path = utils.make_file_copy(source_file_path)
+    else:
+        tmp_file_path = source_file_path
+
+    # Open backup h5 file and copy complet filesystem directory onto a group in h5file
+    with h5py.File(tmp_file_path,'r') as src_file:
+        dest_file_obj.copy(source= src_file['/'], dest= dest_group_name)
+
+    if 'tmp_files' in tmp_file_path:
+        os.remove(tmp_file_path)
+
+    stdout = f'Completed transfer for /{dest_group_name}'
+    return stdout
+
+
+[docs] +def create_hdf5_file_from_filesystem_path(path_to_input_directory: str, + path_to_filenames_dict: dict = None, + select_dir_keywords : list = [], + root_metadata_dict : dict = {}, mode = 'w'): + + """ + Creates an .h5 file with name "output_filename" that preserves the directory tree (or folder structure) + of a given filesystem path. + + The data integration capabilities are limited by our file reader, which can only access data from a list of + admissible file formats. These, however, can be extended. Directories are groups in the resulting HDF5 file. + Files are formatted as composite objects consisting of a group, file, and attributes. + + Parameters + ---------- + output_filename : str + Name of the output HDF5 file. + path_to_input_directory : str + Path to root directory, specified with forward slashes, e.g., path/to/root. + + path_to_filenames_dict : dict, optional + A pre-processed dictionary where keys are directory paths on the input directory's tree and values are lists of files. + If provided, 'input_file_system_path' is ignored. + + select_dir_keywords : list + List of string elements to consider or select only directory paths that contain + a word in 'select_dir_keywords'. When empty, all directory paths are considered + to be included in the HDF5 file group hierarchy. + root_metadata_dict : dict + Metadata to include at the root level of the HDF5 file. + + mode : str + 'w' create File, truncate if it exists, or 'r+' read/write, File must exists. By default, mode = "w". + + Returns + ------- + output_filename : str + Path to the created HDF5 file. + """ + + + if not mode in ['w','r+']: + raise ValueError(f'Parameter mode must take values in ["w","r+"]') + + if not '/' in path_to_input_directory: + raise ValueError('path_to_input_directory needs to be specified using forward slashes "/".' ) + + #path_to_output_directory = os.path.join(path_to_input_directory,'..') + path_to_input_directory = os.path.normpath(path_to_input_directory).rstrip(os.sep) + + + for i, keyword in enumerate(select_dir_keywords): + select_dir_keywords[i] = keyword.replace('/',os.sep) + + if not path_to_filenames_dict: + # On dry_run=True, returns path to files dictionary of the output directory without making a actual copy of the input directory. + # Therefore, there wont be a copying conflict by setting up input and output directories the same + path_to_filenames_dict = utils.copy_directory_with_contraints(input_dir_path=path_to_input_directory, + output_dir_path=path_to_input_directory, + dry_run=True) + # Set input_directory as copied input directory + root_dir = path_to_input_directory + path_to_output_file = path_to_input_directory.rstrip(os.path.sep) + '.h5' + + start_message = f'\n[Start] Data integration :\nSource: {path_to_input_directory}\nDestination: {path_to_output_file}\n' + + print(start_message) + logging.info(start_message) + + # Check if the .h5 file already exists + if os.path.exists(path_to_output_file) and mode in ['w']: + message = ( + f"[Notice] The file '{path_to_output_file}' already exists and will not be overwritten.\n" + "If you wish to replace it, please delete the existing file first and rerun the program." + ) + print(message) + logging.error(message) + else: + with h5py.File(path_to_output_file, mode=mode, track_order=True) as h5file: + + number_of_dirs = len(path_to_filenames_dict.keys()) + dir_number = 1 + for dirpath, filtered_filenames_list in path_to_filenames_dict.items(): + + # Check if filtered_filenames_list is nonempty. TODO: This is perhaps redundant by design of path_to_filenames_dict. + if not filtered_filenames_list: + continue + + group_name = dirpath.replace(os.sep,'/') + group_name = group_name.replace(root_dir.replace(os.sep,'/') + '/', '/') + + # Flatten group name to one level + if select_dir_keywords: + offset = sum([len(i.split(os.sep)) if i in dirpath else 0 for i in select_dir_keywords]) + else: + offset = 1 + tmp_list = group_name.split('/') + if len(tmp_list) > offset+1: + group_name = '/'.join([tmp_list[i] for i in range(offset+1)]) + + # Create group called "group_name". Hierarchy of nested groups can be implicitly defined by the forward slashes + if not group_name in h5file.keys(): + h5file.create_group(group_name) + h5file[group_name].attrs['creation_date'] = utils.created_at().encode('utf-8') + #h5file[group_name].attrs.create(name='filtered_file_list',data=convert_string_to_bytes(filtered_filename_list)) + #h5file[group_name].attrs.create(name='file_list',data=convert_string_to_bytes(filenames_list)) + #else: + #print(group_name,' was already created.') + instFoldermsgStart = f'Starting data transfer from instFolder: {group_name}' + print(instFoldermsgStart) + + for filenumber, filename in enumerate(filtered_filenames_list): + + #file_ext = os.path.splitext(filename)[1] + #try: + + # hdf5 path to filename group + dest_group_name = f'{group_name}/{filename}' + + if not 'h5' in filename: + #file_dict = config_file.select_file_readers(group_id)[file_ext](os.path.join(dirpath,filename)) + #file_dict = ext_to_reader_dict[file_ext](os.path.join(dirpath,filename)) + file_dict = filereader_registry.select_file_reader(dest_group_name)(os.path.join(dirpath,filename)) + + stdout = __transfer_file_dict_to_hdf5(h5file, group_name, file_dict) + + else: + source_file_path = os.path.join(dirpath,filename) + dest_file_obj = h5file + #group_name +'/'+filename + #ext_to_reader_dict[file_ext](source_file_path, dest_file_obj, dest_group_name) + #g5505f_reader.select_file_reader(dest_group_name)(source_file_path, dest_file_obj, dest_group_name) + stdout = __copy_file_in_group(source_file_path, dest_file_obj, dest_group_name, False) + + # Update the progress bar and log the end message + instFoldermsdEnd = f'\nCompleted data transfer for instFolder: {group_name}\n' + # Print and log the start message + utils.progressBar(dir_number, number_of_dirs, instFoldermsdEnd) + logging.info(instFoldermsdEnd ) + dir_number = dir_number + 1 + + print('[End] Data integration') + logging.info('[End] Data integration') + + if len(root_metadata_dict.keys())>0: + for key, value in root_metadata_dict.items(): + #if key in h5file.attrs: + # del h5file.attrs[key] + h5file.attrs.create(key, value) + #annotate_root_dir(output_filename,root_metadata_dict) + + + #output_yml_filename_path = hdf5_vis.take_yml_snapshot_of_hdf5_file(output_filename) + + return path_to_output_file #, output_yml_filename_path
+ + +
+[docs] +def create_hdf5_file_from_dataframe(ofilename, input_data, group_by_funcs: list, approach: str = None, extract_attrs_func=None): + """ + Creates an HDF5 file with hierarchical groups based on the specified grouping functions or columns. + + Parameters: + ----------- + ofilename (str): Path for the output HDF5 file. + input_data (pd.DataFrame or str): Input data as a DataFrame or a valid file system path. + group_by_funcs (list): List of callables or column names to define hierarchical grouping. + approach (str): Specifies the approach ('top-down' or 'bottom-up') for creating the HDF5 file. + extract_attrs_func (callable, optional): Function to extract additional attributes for HDF5 groups. + + Returns: + -------- + None + """ + # Check whether input_data is a valid file-system path or a DataFrame + is_valid_path = lambda x: os.path.exists(x) if isinstance(x, str) else False + + if is_valid_path(input_data): + # If input_data is a file-system path, create a DataFrame with file info + file_list = os.listdir(input_data) + df = pd.DataFrame(file_list, columns=['filename']) + df = utils.augment_with_filetype(df) # Add filetype information if needed + elif isinstance(input_data, pd.DataFrame): + # If input_data is a DataFrame, make a copy + df = input_data.copy() + else: + raise ValueError("input_data must be either a valid file-system path or a DataFrame.") + + # Generate grouping columns based on group_by_funcs + if utils.is_callable_list(group_by_funcs): + grouping_cols = [] + for i, func in enumerate(group_by_funcs): + col_name = f'level_{i}_groups' + grouping_cols.append(col_name) + df[col_name] = func(df) + elif utils.is_str_list(group_by_funcs) and all([item in df.columns for item in group_by_funcs]): + grouping_cols = group_by_funcs + else: + raise ValueError("'group_by_funcs' must be a list of callables or valid column names in the DataFrame.") + + # Generate group paths + df['group_path'] = ['/' + '/'.join(row) for row in df[grouping_cols].values.astype(str)] + + # Open the HDF5 file in write mode + with h5py.File(ofilename, 'w') as file: + for group_path in df['group_path'].unique(): + # Create groups in HDF5 + group = file.create_group(group_path) + + # Filter the DataFrame for the current group + datatable = df[df['group_path'] == group_path].copy() + + # Drop grouping columns and the generated 'group_path' + datatable = datatable.drop(columns=grouping_cols + ['group_path']) + + # Add datasets to groups if data exists + if not datatable.empty: + dataset = utils.convert_dataframe_to_np_structured_array(datatable) + group.create_dataset(name='data_table', data=dataset) + + # Add attributes if extract_attrs_func is provided + if extract_attrs_func: + attrs = extract_attrs_func(datatable) + for key, value in attrs.items(): + group.attrs[key] = value + + # Save metadata about depth of hierarchy + file.attrs.create(name='depth', data=len(grouping_cols) - 1) + + print(f"HDF5 file created successfully at {ofilename}") + + return ofilename
+ + + +
+[docs] +def save_processed_dataframe_to_hdf5(df, annotator, output_filename): # src_hdf5_path, script_date, script_name): + """ + Save processed dataframe columns with annotations to an HDF5 file. + + Parameters: + df (pd.DataFrame): DataFrame containing processed time series. + annotator (): Annotator object with get_metadata method. + output_filename (str): Path to the source HDF5 file. + """ + # Convert datetime columns to string + datetime_cols = df.select_dtypes(include=['datetime64']).columns + + if list(datetime_cols): + df[datetime_cols] = df[datetime_cols].map(str) + + # Convert dataframe to structured array + icad_data_table = utils.convert_dataframe_to_np_structured_array(df) + + # Get metadata + metadata_dict = annotator.get_metadata() + + # Prepare project level attributes to be added at the root level + + project_level_attributes = metadata_dict['metadata']['project'] + + # Prepare high-level attributes + high_level_attributes = { + 'parent_files': metadata_dict['parent_files'], + **metadata_dict['metadata']['sample'], + **metadata_dict['metadata']['environment'], + **metadata_dict['metadata']['instruments'] + } + + # Prepare data level attributes + data_level_attributes = metadata_dict['metadata']['datasets'] + + for key, value in data_level_attributes.items(): + if isinstance(value,dict): + data_level_attributes[key] = utils.convert_attrdict_to_np_structured_array(value) + + + # Prepare file dictionary + file_dict = { + 'name': project_level_attributes['processing_file'], + 'attributes_dict': high_level_attributes, + 'datasets': [{ + 'name': "data_table", + 'data': icad_data_table, + 'shape': icad_data_table.shape, + 'attributes': data_level_attributes + }] + } + + # Check if the file exists + if os.path.exists(output_filename): + mode = "a" + print(f"File {output_filename} exists. Opening in append mode.") + else: + mode = "w" + print(f"File {output_filename} does not exist. Creating a new file.") + + + # Write to HDF5 + with h5py.File(output_filename, mode) as h5file: + # Add project level attributes at the root/top level + h5file.attrs.update(project_level_attributes) + __transfer_file_dict_to_hdf5(h5file, '/', file_dict)
+ + +#if __name__ == '__main__': +
+ +
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/docs/build/html/_modules/src/metadata_review_lib.html b/docs/build/html/_modules/src/metadata_review_lib.html index 259e989..f79f293 100644 --- a/docs/build/html/_modules/src/metadata_review_lib.html +++ b/docs/build/html/_modules/src/metadata_review_lib.html @@ -1,545 +1,545 @@ - - - - - - src.metadata_review_lib — DIMA 1.0.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for src.metadata_review_lib

-import sys
-import os
-root_dir = os.path.abspath(os.curdir)
-sys.path.append(root_dir)
-import subprocess
-
-import h5py
-import yaml
-import src.g5505_utils as utils
-import src.hdf5_vis as hdf5_vis
-import src.hdf5_lib as hdf5_lib
-import src.git_ops as git_ops
-
-
-import numpy as np
-
-
-
-YAML_EXT = ".yaml"
-TXT_EXT = ".txt"
-
-
-
-
-[docs] -def get_review_status(filename_path): - - filename_path_tail, filename_path_head = os.path.split(filename_path) - filename, ext = os.path.splitext(filename_path_head) - # TODO: - with open(os.path.join("review/",filename+"-review_status"+TXT_EXT),'r') as f: - workflow_steps = [] - for line in f: - workflow_steps.append(line) - return workflow_steps[-1]
- - -
-[docs] -def first_initialize_metadata_review(hdf5_file_path, reviewer_attrs, restart = False): - - """ - First: Initialize review branch with review folder with a copy of yaml representation of - hdf5 file under review and by creating a txt file with the state of the review process, e.g., under review. - - """ - - initials = reviewer_attrs['initials'] - #branch_name = '-'.join([reviewer_attrs['type'],'review_',initials]) - branch_name = '_'.join(['review',initials]) - - hdf5_file_path_tail, filename_path_head = os.path.split(hdf5_file_path) - filename, ext = os.path.splitext(filename_path_head) - - # Check file_path points to h5 file - if not 'h5' in ext: - raise ValueError("filename_path needs to point to an h5 file.") - - # Verify if yaml snapshot of input h5 file exists - if not os.path.exists(os.path.join(hdf5_file_path_tail,filename+YAML_EXT)): - raise ValueError("metadata review cannot be initialized. The associated .yaml file under review was not found. Run take_yml_snapshot_of_hdf5_file(filename_path) ") - - # Initialize metadata review workflow - # print("Create branch metadata-review-by-"+initials+"\n") - - #checkout_review_branch(branch_name) - - # Check you are working at the right branch - - curr_branch = git_ops.show_current_branch() - if not branch_name in curr_branch.stdout: - raise ValueError("Branch "+branch_name+" was not found. \nPlease open a Git Bash Terminal, and follow the below instructions: \n1. Change directory to your project's directory. \n2. Excecute the command: git checkout "+branch_name) - - # Check if review file already exists and then check if it is still untracked - review_yaml_file_path = os.path.join("review/",filename+YAML_EXT) - review_yaml_file_path_tail, ext = os.path.splitext(review_yaml_file_path) - review_status_yaml_file_path = os.path.join(review_yaml_file_path_tail+"-review_status"+".txt") - - if not os.path.exists(review_yaml_file_path) or restart: - review_yaml_file_path = utils.make_file_copy(os.path.join(hdf5_file_path_tail,filename+YAML_EXT), 'review') - if restart: - print('metadata review has been reinitialized. The review files will reflect the current state of the hdf5 files metadata') - - - - #if not os.path.exists(os.path.join(review_yaml_file_path_tail+"-review_status"+".txt")): - - with open(review_status_yaml_file_path,'w') as f: - f.write('under review') - - # Stage untracked review files and commit them to local repository - status = git_ops.get_status() - untracked_files = [] - for line in status.stdout.splitlines(): - #tmp = line.decode("utf-8") - #modified_files.append(tmp.split()[1]) - if 'review/' in line: - if not 'modified' in line: # untracked filesand - untracked_files.append(line.strip()) - else: - untracked_files.append(line.strip().split()[1]) - - if 'output_files/'+filename+YAML_EXT in line and not 'modified' in line: - untracked_files.append(line.strip()) - - if untracked_files: - result = subprocess.run(git_ops.add_files_to_git(untracked_files),capture_output=True,check=True) - message = 'Initialized metadata review.' - commit_output = subprocess.run(git_ops.commit_changes(message),capture_output=True,check=True) - - for line in commit_output.stdout.splitlines(): - print(line.decode('utf-8')) - #else: - # print('This action will not have any effect because metadata review process has been already initialized.') - - - - - #status_dict = repo_obj.status() - #for filepath, file_status in status_dict.items(): - # Identify keys associated to review files and stage them - # if 'review/'+filename in filepath: - # Stage changes - # repo_obj.index.add(filepath) - - #author = config_file.author #default_signature - #committer = config_file.committer - #message = "Initialized metadata review process." - #tree = repo_obj.index.write_tree() - #oid = repo_obj.create_commit('HEAD', author, committer, message, tree, [repo_obj.head.peel().oid]) - - #print("Add and commit"+"\n") - - return review_yaml_file_path, review_status_yaml_file_path
- - - - -
-[docs] -def second_save_metadata_review(review_yaml_file_path, reviewer_attrs): - """ - Second: Once you're done reviewing the yaml representation of hdf5 file in review folder. - Change the review status to complete and save (add and commit) modified .yalm and .txt files in the project by - running this function. - - """ - # 1 verify review initializatin was performed first - # 2. change review status in txt to complete - # 3. git add review/ and git commit -m "Submitted metadata review" - - initials = reviewer_attrs['initials'] - #branch_name = '-'.join([reviewer_attrs['type'],'review','by',initials]) - branch_name = '_'.join(['review',initials]) - # TODO: replace with subprocess + git - #checkout_review_branch(repo_obj, branch_name) - - # Check you are working at the right branch - curr_branch = git_ops.show_current_branch() - if not branch_name in curr_branch.stdout: - raise ValueError('Please checkout ' + branch_name + ' via Git Bash before submitting metadata review files. ') - - # Collect modified review files - status = git_ops.get_status() - modified_files = [] - os.path.basename(review_yaml_file_path) - for line in status.stdout.splitlines(): - # conver line from bytes to str - tmp = line.decode("utf-8") - if 'modified' in tmp and 'review/' in tmp and os.path.basename(review_yaml_file_path) in tmp: - modified_files.append(tmp.split()[1]) - - # Stage modified files and commit them to local repository - review_yaml_file_path_tail, review_yaml_file_path_head = os.path.split(review_yaml_file_path) - filename, ext = os.path.splitext(review_yaml_file_path_head) - if modified_files: - review_status_file_path = os.path.join("review/",filename+"-review_status"+TXT_EXT) - with open(review_status_file_path,'a') as f: - f.write('\nsubmitted') - - modified_files.append(review_status_file_path) - - result = subprocess.run(git_ops.add_files_to_git(modified_files),capture_output=True,check=True) - message = 'Submitted metadata review.' - commit_output = subprocess.run(git_ops.commit_changes(message),capture_output=True,check=True) - - for line in commit_output.stdout.splitlines(): - print(line.decode('utf-8')) - else: - print('Nothing to commit.')
- - -# -
-[docs] -def load_yaml(yaml_review_file): - with open(yaml_review_file, 'r') as stream: - try: - return yaml.load(stream, Loader=yaml.FullLoader) - except yaml.YAMLError as exc: - print(exc) - return None
- - -
-[docs] -def update_hdf5_attributes(input_hdf5_file, yaml_dict): - - def update_attributes(hdf5_obj, yaml_obj): - for attr_name, attr_value in yaml_obj['attributes'].items(): - - if not isinstance(attr_value, dict): - attr_value = {'rename_as': attr_name, 'value': attr_value} - - if (attr_name in hdf5_obj.attrs.keys()): # delete or update - if attr_value.get('delete'): # delete when True - hdf5_obj.attrs.__delitem__(attr_name) - elif not (attr_value.get('rename_as') == attr_name): # update when true - hdf5_obj.attrs[attr_value.get('rename_as')] = hdf5_obj.attrs[attr_name] # parse_attribute(attr_value) - hdf5_obj.attrs.__delitem__(attr_name) - else: # add a new attribute - hdf5_obj.attrs.update({attr_name : utils.parse_attribute(attr_value)}) - - with h5py.File(input_hdf5_file, 'r+') as f: - for key in yaml_dict.keys(): - hdf5_obj = f[key] - yaml_obj = yaml_dict[key] - update_attributes(hdf5_obj, yaml_obj)
- - -
-[docs] -def update_hdf5_file_with_review(input_hdf5_file, yaml_review_file): - yaml_dict = load_yaml(yaml_review_file) - update_hdf5_attributes(input_hdf5_file, yaml_dict) - # Regenerate yaml snapshot of updated HDF5 file - output_yml_filename_path = hdf5_vis.take_yml_snapshot_of_hdf5_file(input_hdf5_file) - print(f'{output_yml_filename_path} was successfully regenerated from the updated version of{input_hdf5_file}')
- - -
-[docs] -def third_update_hdf5_file_with_review(input_hdf5_file, yaml_review_file, reviewer_attrs={}, hdf5_upload=False): - if 'submitted' not in get_review_status(input_hdf5_file): - raise ValueError('Review yaml file must be submitted before trying to perform an update. Run first second_submit_metadata_review().') - - update_hdf5_file_with_review(input_hdf5_file, yaml_review_file) - git_ops.perform_git_operations(hdf5_upload)
- - -
-[docs] -def count(hdf5_obj,yml_dict): - print(hdf5_obj.name) - if isinstance(hdf5_obj,h5py.Group) and len(hdf5_obj.name.split('/')) <= 4: - obj_review = yml_dict[hdf5_obj.name] - additions = [not (item in hdf5_obj.attrs.keys()) for item in obj_review['attributes'].keys()] - count_additions = sum(additions) - deletions = [not (item in obj_review['attributes'].keys()) for item in hdf5_obj.attrs.keys()] - count_delections = sum(deletions) - print('additions',count_additions, 'deletions', count_delections)
- - -
-[docs] -def last_submit_metadata_review(reviewer_attrs): - - """Fourth: """ - - initials =reviewer_attrs['initials'] - - repository = 'origin' - branch_name = '_'.join(['review',initials]) - - push_command = lambda repository,refspec: ['git','push',repository,refspec] - - list_branches_command = ['git','branch','--list'] - - branches = subprocess.run(list_branches_command,capture_output=True,text=True,check=True) - if not branch_name in branches.stdout: - print('There is no branch named '+branch_name+'.\n') - print('Make sure to run data owner review workflow from the beginning without missing any steps.') - return - - curr_branch = git_ops.show_current_branch() - if not branch_name in curr_branch.stdout: - print('Complete metadata review could not be completed.\n') - print('Make sure a data-owner workflow has already been started on branch '+branch_name+'\n') - print('The step "Complete metadata review" will have no effect.') - return - - - - # push - result = subprocess.run(push_command(repository,branch_name),capture_output=True,text=True,check=True) - print(result.stdout) - - # 1. git add output_files/ - # 2. delete review/ - #shutil.rmtree(os.path.join(os.path.abspath(os.curdir),"review")) - # 3. git rm review/ - # 4. git commit -m "Completed review process. Current state of hdf5 file and yml should be up to date." - return result.returncode
- - - -#import config_file -#import hdf5_vis - -
-[docs] -class MetadataHarvester: - def __init__(self, parent_files=None): - if parent_files is None: - parent_files = [] - self.parent_files = parent_files - self.metadata = { - "project": {}, - "sample": {}, - "environment": {}, - "instruments": {}, - "datasets": {} - } - -
-[docs] - def add_project_info(self, key_or_dict, value=None, append=False): - self._add_info("project", key_or_dict, value, append)
- - -
-[docs] - def add_sample_info(self, key_or_dict, value=None, append=False): - self._add_info("sample", key_or_dict, value, append)
- - -
-[docs] - def add_environment_info(self, key_or_dict, value=None, append=False): - self._add_info("environment", key_or_dict, value, append)
- - -
-[docs] - def add_instrument_info(self, key_or_dict, value=None, append=False): - self._add_info("instruments", key_or_dict, value, append)
- - -
-[docs] - def add_dataset_info(self, key_or_dict, value=None, append=False): - self._add_info("datasets", key_or_dict, value, append)
- - - def _add_info(self, category, key_or_dict, value, append): - """Internal helper method to add information to a category.""" - if isinstance(key_or_dict, dict): - self.metadata[category].update(key_or_dict) - else: - if key_or_dict in self.metadata[category]: - if append: - current_value = self.metadata[category][key_or_dict] - - if isinstance(current_value, list): - - if not isinstance(value, list): - # Append the new value to the list - self.metadata[category][key_or_dict].append(value) - else: - self.metadata[category][key_or_dict] = current_value + value - - elif isinstance(current_value, str): - # Append the new value as a comma-separated string - self.metadata[category][key_or_dict] = current_value + ',' + str(value) - else: - # Handle other types (for completeness, usually not required) - self.metadata[category][key_or_dict] = [current_value, value] - else: - self.metadata[category][key_or_dict] = value - else: - self.metadata[category][key_or_dict] = value - -
-[docs] - def get_metadata(self): - return { - "parent_files": self.parent_files, - "metadata": self.metadata - }
- - -
-[docs] - def print_metadata(self): - print("parent_files", self.parent_files) - - for key in self.metadata.keys(): - print(key,'metadata:\n') - for item in self.metadata[key].items(): - print(item[0],item[1])
- - - - -
-[docs] - def clear_metadata(self): - self.metadata = { - "project": {}, - "sample": {}, - "environment": {}, - "instruments": {}, - "datasets": {} - } - self.parent_files = []
-
- - -
-[docs] -def main(): - - output_filename_path = "output_files/unified_file_smog_chamber_2024-03-19_UTC-OFST_+0100_NG.h5" - output_yml_filename_path = "output_files/unified_file_smog_chamber_2024-03-19_UTC-OFST_+0100_NG.yalm" - output_yml_filename_path_tail, filename = os.path.split(output_yml_filename_path)
- - #output_yml_filename_path = hdf5_vis.take_yml_snapshot_of_hdf5_file(output_filename_path) - - #first_initialize_metadata_review(output_filename_path,initials='NG') - #second_submit_metadata_review() - #if os.path.exists(os.path.join(os.path.join(os.path.abspath(os.curdir),"review"),filename)): - # third_update_hdf5_file_with_review(output_filename_path, os.path.join(os.path.join(os.path.abspath(os.curdir),"review"),filename)) - #fourth_complete_metadata_review() - -#if __name__ == '__main__': - -# main() -
- -
-
- -
-
-
-
- - - + + + + + + src.metadata_review_lib — DIMA 1.0.0 documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for src.metadata_review_lib

+import sys
+import os
+root_dir = os.path.abspath(os.curdir)
+sys.path.append(root_dir)
+import subprocess
+
+import h5py
+import yaml
+import src.g5505_utils as utils
+import src.hdf5_vis as hdf5_vis
+import src.hdf5_lib as hdf5_lib
+import src.git_ops as git_ops
+
+
+import numpy as np
+
+
+
+YAML_EXT = ".yaml"
+TXT_EXT = ".txt"
+
+
+
+
+[docs] +def get_review_status(filename_path): + + filename_path_tail, filename_path_head = os.path.split(filename_path) + filename, ext = os.path.splitext(filename_path_head) + # TODO: + with open(os.path.join("review/",filename+"-review_status"+TXT_EXT),'r') as f: + workflow_steps = [] + for line in f: + workflow_steps.append(line) + return workflow_steps[-1]
+ + +
+[docs] +def first_initialize_metadata_review(hdf5_file_path, reviewer_attrs, restart = False): + + """ + First: Initialize review branch with review folder with a copy of yaml representation of + hdf5 file under review and by creating a txt file with the state of the review process, e.g., under review. + + """ + + initials = reviewer_attrs['initials'] + #branch_name = '-'.join([reviewer_attrs['type'],'review_',initials]) + branch_name = '_'.join(['review',initials]) + + hdf5_file_path_tail, filename_path_head = os.path.split(hdf5_file_path) + filename, ext = os.path.splitext(filename_path_head) + + # Check file_path points to h5 file + if not 'h5' in ext: + raise ValueError("filename_path needs to point to an h5 file.") + + # Verify if yaml snapshot of input h5 file exists + if not os.path.exists(os.path.join(hdf5_file_path_tail,filename+YAML_EXT)): + raise ValueError("metadata review cannot be initialized. The associated .yaml file under review was not found. Run take_yml_snapshot_of_hdf5_file(filename_path) ") + + # Initialize metadata review workflow + # print("Create branch metadata-review-by-"+initials+"\n") + + #checkout_review_branch(branch_name) + + # Check you are working at the right branch + + curr_branch = git_ops.show_current_branch() + if not branch_name in curr_branch.stdout: + raise ValueError("Branch "+branch_name+" was not found. \nPlease open a Git Bash Terminal, and follow the below instructions: \n1. Change directory to your project's directory. \n2. Excecute the command: git checkout "+branch_name) + + # Check if review file already exists and then check if it is still untracked + review_yaml_file_path = os.path.join("review/",filename+YAML_EXT) + review_yaml_file_path_tail, ext = os.path.splitext(review_yaml_file_path) + review_status_yaml_file_path = os.path.join(review_yaml_file_path_tail+"-review_status"+".txt") + + if not os.path.exists(review_yaml_file_path) or restart: + review_yaml_file_path = utils.make_file_copy(os.path.join(hdf5_file_path_tail,filename+YAML_EXT), 'review') + if restart: + print('metadata review has been reinitialized. The review files will reflect the current state of the hdf5 files metadata') + + + + #if not os.path.exists(os.path.join(review_yaml_file_path_tail+"-review_status"+".txt")): + + with open(review_status_yaml_file_path,'w') as f: + f.write('under review') + + # Stage untracked review files and commit them to local repository + status = git_ops.get_status() + untracked_files = [] + for line in status.stdout.splitlines(): + #tmp = line.decode("utf-8") + #modified_files.append(tmp.split()[1]) + if 'review/' in line: + if not 'modified' in line: # untracked filesand + untracked_files.append(line.strip()) + else: + untracked_files.append(line.strip().split()[1]) + + if 'output_files/'+filename+YAML_EXT in line and not 'modified' in line: + untracked_files.append(line.strip()) + + if untracked_files: + result = subprocess.run(git_ops.add_files_to_git(untracked_files),capture_output=True,check=True) + message = 'Initialized metadata review.' + commit_output = subprocess.run(git_ops.commit_changes(message),capture_output=True,check=True) + + for line in commit_output.stdout.splitlines(): + print(line.decode('utf-8')) + #else: + # print('This action will not have any effect because metadata review process has been already initialized.') + + + + + #status_dict = repo_obj.status() + #for filepath, file_status in status_dict.items(): + # Identify keys associated to review files and stage them + # if 'review/'+filename in filepath: + # Stage changes + # repo_obj.index.add(filepath) + + #author = config_file.author #default_signature + #committer = config_file.committer + #message = "Initialized metadata review process." + #tree = repo_obj.index.write_tree() + #oid = repo_obj.create_commit('HEAD', author, committer, message, tree, [repo_obj.head.peel().oid]) + + #print("Add and commit"+"\n") + + return review_yaml_file_path, review_status_yaml_file_path
+ + + + +
+[docs] +def second_save_metadata_review(review_yaml_file_path, reviewer_attrs): + """ + Second: Once you're done reviewing the yaml representation of hdf5 file in review folder. + Change the review status to complete and save (add and commit) modified .yalm and .txt files in the project by + running this function. + + """ + # 1 verify review initializatin was performed first + # 2. change review status in txt to complete + # 3. git add review/ and git commit -m "Submitted metadata review" + + initials = reviewer_attrs['initials'] + #branch_name = '-'.join([reviewer_attrs['type'],'review','by',initials]) + branch_name = '_'.join(['review',initials]) + # TODO: replace with subprocess + git + #checkout_review_branch(repo_obj, branch_name) + + # Check you are working at the right branch + curr_branch = git_ops.show_current_branch() + if not branch_name in curr_branch.stdout: + raise ValueError('Please checkout ' + branch_name + ' via Git Bash before submitting metadata review files. ') + + # Collect modified review files + status = git_ops.get_status() + modified_files = [] + os.path.basename(review_yaml_file_path) + for line in status.stdout.splitlines(): + # conver line from bytes to str + tmp = line.decode("utf-8") + if 'modified' in tmp and 'review/' in tmp and os.path.basename(review_yaml_file_path) in tmp: + modified_files.append(tmp.split()[1]) + + # Stage modified files and commit them to local repository + review_yaml_file_path_tail, review_yaml_file_path_head = os.path.split(review_yaml_file_path) + filename, ext = os.path.splitext(review_yaml_file_path_head) + if modified_files: + review_status_file_path = os.path.join("review/",filename+"-review_status"+TXT_EXT) + with open(review_status_file_path,'a') as f: + f.write('\nsubmitted') + + modified_files.append(review_status_file_path) + + result = subprocess.run(git_ops.add_files_to_git(modified_files),capture_output=True,check=True) + message = 'Submitted metadata review.' + commit_output = subprocess.run(git_ops.commit_changes(message),capture_output=True,check=True) + + for line in commit_output.stdout.splitlines(): + print(line.decode('utf-8')) + else: + print('Nothing to commit.')
+ + +# +
+[docs] +def load_yaml(yaml_review_file): + with open(yaml_review_file, 'r') as stream: + try: + return yaml.load(stream, Loader=yaml.FullLoader) + except yaml.YAMLError as exc: + print(exc) + return None
+ + +
+[docs] +def update_hdf5_attributes(input_hdf5_file, yaml_dict): + + def update_attributes(hdf5_obj, yaml_obj): + for attr_name, attr_value in yaml_obj['attributes'].items(): + + if not isinstance(attr_value, dict): + attr_value = {'rename_as': attr_name, 'value': attr_value} + + if (attr_name in hdf5_obj.attrs.keys()): # delete or update + if attr_value.get('delete'): # delete when True + hdf5_obj.attrs.__delitem__(attr_name) + elif not (attr_value.get('rename_as') == attr_name): # update when true + hdf5_obj.attrs[attr_value.get('rename_as')] = hdf5_obj.attrs[attr_name] # parse_attribute(attr_value) + hdf5_obj.attrs.__delitem__(attr_name) + else: # add a new attribute + hdf5_obj.attrs.update({attr_name : utils.parse_attribute(attr_value)}) + + with h5py.File(input_hdf5_file, 'r+') as f: + for key in yaml_dict.keys(): + hdf5_obj = f[key] + yaml_obj = yaml_dict[key] + update_attributes(hdf5_obj, yaml_obj)
+ + +
+[docs] +def update_hdf5_file_with_review(input_hdf5_file, yaml_review_file): + yaml_dict = load_yaml(yaml_review_file) + update_hdf5_attributes(input_hdf5_file, yaml_dict) + # Regenerate yaml snapshot of updated HDF5 file + output_yml_filename_path = hdf5_vis.take_yml_snapshot_of_hdf5_file(input_hdf5_file) + print(f'{output_yml_filename_path} was successfully regenerated from the updated version of{input_hdf5_file}')
+ + +
+[docs] +def third_update_hdf5_file_with_review(input_hdf5_file, yaml_review_file, reviewer_attrs={}, hdf5_upload=False): + if 'submitted' not in get_review_status(input_hdf5_file): + raise ValueError('Review yaml file must be submitted before trying to perform an update. Run first second_submit_metadata_review().') + + update_hdf5_file_with_review(input_hdf5_file, yaml_review_file) + git_ops.perform_git_operations(hdf5_upload)
+ + +
+[docs] +def count(hdf5_obj,yml_dict): + print(hdf5_obj.name) + if isinstance(hdf5_obj,h5py.Group) and len(hdf5_obj.name.split('/')) <= 4: + obj_review = yml_dict[hdf5_obj.name] + additions = [not (item in hdf5_obj.attrs.keys()) for item in obj_review['attributes'].keys()] + count_additions = sum(additions) + deletions = [not (item in obj_review['attributes'].keys()) for item in hdf5_obj.attrs.keys()] + count_delections = sum(deletions) + print('additions',count_additions, 'deletions', count_delections)
+ + +
+[docs] +def last_submit_metadata_review(reviewer_attrs): + + """Fourth: """ + + initials =reviewer_attrs['initials'] + + repository = 'origin' + branch_name = '_'.join(['review',initials]) + + push_command = lambda repository,refspec: ['git','push',repository,refspec] + + list_branches_command = ['git','branch','--list'] + + branches = subprocess.run(list_branches_command,capture_output=True,text=True,check=True) + if not branch_name in branches.stdout: + print('There is no branch named '+branch_name+'.\n') + print('Make sure to run data owner review workflow from the beginning without missing any steps.') + return + + curr_branch = git_ops.show_current_branch() + if not branch_name in curr_branch.stdout: + print('Complete metadata review could not be completed.\n') + print('Make sure a data-owner workflow has already been started on branch '+branch_name+'\n') + print('The step "Complete metadata review" will have no effect.') + return + + + + # push + result = subprocess.run(push_command(repository,branch_name),capture_output=True,text=True,check=True) + print(result.stdout) + + # 1. git add output_files/ + # 2. delete review/ + #shutil.rmtree(os.path.join(os.path.abspath(os.curdir),"review")) + # 3. git rm review/ + # 4. git commit -m "Completed review process. Current state of hdf5 file and yml should be up to date." + return result.returncode
+ + + +#import config_file +#import hdf5_vis + +
+[docs] +class MetadataHarvester: + def __init__(self, parent_files=None): + if parent_files is None: + parent_files = [] + self.parent_files = parent_files + self.metadata = { + "project": {}, + "sample": {}, + "environment": {}, + "instruments": {}, + "datasets": {} + } + +
+[docs] + def add_project_info(self, key_or_dict, value=None, append=False): + self._add_info("project", key_or_dict, value, append)
+ + +
+[docs] + def add_sample_info(self, key_or_dict, value=None, append=False): + self._add_info("sample", key_or_dict, value, append)
+ + +
+[docs] + def add_environment_info(self, key_or_dict, value=None, append=False): + self._add_info("environment", key_or_dict, value, append)
+ + +
+[docs] + def add_instrument_info(self, key_or_dict, value=None, append=False): + self._add_info("instruments", key_or_dict, value, append)
+ + +
+[docs] + def add_dataset_info(self, key_or_dict, value=None, append=False): + self._add_info("datasets", key_or_dict, value, append)
+ + + def _add_info(self, category, key_or_dict, value, append): + """Internal helper method to add information to a category.""" + if isinstance(key_or_dict, dict): + self.metadata[category].update(key_or_dict) + else: + if key_or_dict in self.metadata[category]: + if append: + current_value = self.metadata[category][key_or_dict] + + if isinstance(current_value, list): + + if not isinstance(value, list): + # Append the new value to the list + self.metadata[category][key_or_dict].append(value) + else: + self.metadata[category][key_or_dict] = current_value + value + + elif isinstance(current_value, str): + # Append the new value as a comma-separated string + self.metadata[category][key_or_dict] = current_value + ',' + str(value) + else: + # Handle other types (for completeness, usually not required) + self.metadata[category][key_or_dict] = [current_value, value] + else: + self.metadata[category][key_or_dict] = value + else: + self.metadata[category][key_or_dict] = value + +
+[docs] + def get_metadata(self): + return { + "parent_files": self.parent_files, + "metadata": self.metadata + }
+ + +
+[docs] + def print_metadata(self): + print("parent_files", self.parent_files) + + for key in self.metadata.keys(): + print(key,'metadata:\n') + for item in self.metadata[key].items(): + print(item[0],item[1])
+ + + + +
+[docs] + def clear_metadata(self): + self.metadata = { + "project": {}, + "sample": {}, + "environment": {}, + "instruments": {}, + "datasets": {} + } + self.parent_files = []
+
+ + +
+[docs] +def main(): + + output_filename_path = "output_files/unified_file_smog_chamber_2024-03-19_UTC-OFST_+0100_NG.h5" + output_yml_filename_path = "output_files/unified_file_smog_chamber_2024-03-19_UTC-OFST_+0100_NG.yalm" + output_yml_filename_path_tail, filename = os.path.split(output_yml_filename_path)
+ + #output_yml_filename_path = hdf5_vis.take_yml_snapshot_of_hdf5_file(output_filename_path) + + #first_initialize_metadata_review(output_filename_path,initials='NG') + #second_submit_metadata_review() + #if os.path.exists(os.path.join(os.path.join(os.path.abspath(os.curdir),"review"),filename)): + # third_update_hdf5_file_with_review(output_filename_path, os.path.join(os.path.join(os.path.abspath(os.curdir),"review"),filename)) + #fourth_complete_metadata_review() + +#if __name__ == '__main__': + +# main() +
+ +
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/docs/build/html/_modules/utils/g5505_utils.html b/docs/build/html/_modules/utils/g5505_utils.html index 1a4e5ae..d39cde8 100644 --- a/docs/build/html/_modules/utils/g5505_utils.html +++ b/docs/build/html/_modules/utils/g5505_utils.html @@ -1,565 +1,565 @@ - - - - - - utils.g5505_utils — DIMA 1.0.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for utils.g5505_utils

-import pandas as pd
-import os
-import sys
-import shutil
-import datetime
-import logging
-import numpy as np
-import h5py
-import re
-
-
-
-[docs] -def setup_logging(log_dir, log_filename): - """Sets up logging to a specified directory and file. - - Parameters: - log_dir (str): Directory to save the log file. - log_filename (str): Name of the log file. - """ - # Ensure the log directory exists - os.makedirs(log_dir, exist_ok=True) - - # Create a logger instance - logger = logging.getLogger() - logger.setLevel(logging.INFO) - - # Create a file handler - log_path = os.path.join(log_dir, log_filename) - file_handler = logging.FileHandler(log_path) - - # Create a formatter and set it for the handler - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - file_handler.setFormatter(formatter) - - # Add the handler to the logger - logger.addHandler(file_handler)
- - - -
-[docs] -def is_callable_list(x : list): - return all([callable(item) for item in x])
- - -
-[docs] -def is_str_list(x : list): - return all([isinstance(item,str) for item in x])
- - -
-[docs] -def augment_with_filetype(df): - df['filetype'] = [os.path.splitext(item)[1][1::] for item in df['filename']] - #return [os.path.splitext(item)[1][1::] for item in df['filename']] - return df
- - -
-[docs] -def augment_with_filenumber(df): - df['filenumber'] = [item[0:item.find('_')] for item in df['filename']] - #return [item[0:item.find('_')] for item in df['filename']] - return df
- - -
-[docs] -def group_by_df_column(df, column_name: str): - """ - df (pandas.DataFrame): - column_name (str): column_name of df by which grouping operation will take place. - """ - - if not column_name in df.columns: - raise ValueError("column_name must be in the columns of df.") - - return df[column_name]
- - -
-[docs] -def split_sample_col_into_sample_and_data_quality_cols(input_data: pd.DataFrame): - - sample_name = [] - sample_quality = [] - for item in input_data['sample']: - if item.find('(')!=-1: - #print(item) - sample_name.append(item[0:item.find('(')]) - sample_quality.append(item[item.find('(')+1:len(item)-1]) - else: - if item=='': - sample_name.append('Not yet annotated') - sample_quality.append('unevaluated') - else: - sample_name.append(item) - sample_quality.append('good data') - input_data['sample'] = sample_name - input_data['data_quality'] = sample_quality - - return input_data
- - -
-[docs] -def make_file_copy(source_file_path, output_folder_name : str = 'tmp_files'): - - pathtail, filename = os.path.split(source_file_path) - #backup_filename = 'backup_'+ filename - backup_filename = filename - # Path - ROOT_DIR = os.path.abspath(os.curdir) - - tmp_dirpath = os.path.join(ROOT_DIR,output_folder_name) - if not os.path.exists(tmp_dirpath): - os.mkdir(tmp_dirpath) - - tmp_file_path = os.path.join(tmp_dirpath,backup_filename) - shutil.copy(source_file_path, tmp_file_path) - - return tmp_file_path
- - -
-[docs] -def created_at(datetime_format = '%Y-%m-%d %H:%M:%S'): - now = datetime.datetime.now() - # Populate now object with time zone information obtained from the local system - now_tz_aware = now.astimezone() - tz = now_tz_aware.strftime('%z') - # Replace colons in the time part of the timestamp with hyphens to make it file name friendly - created_at = now_tz_aware.strftime(datetime_format) #+ '_UTC-OFST_' + tz - return created_at
- - -
-[docs] -def sanitize_dataframe(df: pd.DataFrame) -> pd.DataFrame: - # Handle datetime columns (convert to string in 'yyyy-mm-dd hh:mm:ss' format) - datetime_cols = df.select_dtypes(include=['datetime']).columns - for col in datetime_cols: - # Convert datetime to string in the specified format, handling NaT - df[col] = df[col].dt.strftime('%Y-%m-%d %H-%M-%S') - - # Handle object columns with mixed types - otype_cols = df.select_dtypes(include='O') - for col in otype_cols: - col_data = df[col] - - # Check if all elements in the column are strings - if col_data.apply(lambda x: isinstance(x, str)).all(): - df[col] = df[col].astype(str) - else: - # If the column contains mixed types, attempt to convert to numeric, coercing errors to NaN - df[col] = pd.to_numeric(col_data, errors='coerce') - - # Handle NaN values differently based on dtype - if pd.api.types.is_string_dtype(df[col]): - # Replace NaN in string columns with empty string - df[col] = df[col].fillna('') # Replace NaN with empty string - elif pd.api.types.is_numeric_dtype(df[col]): - # For numeric columns, we want to keep NaN as it is - # But if integer column has NaN, consider casting to float - if pd.api.types.is_integer_dtype(df[col]): - df[col] = df[col].astype(float) # Cast to float to allow NaN - else: - df[col] = df[col].fillna(np.nan) # Keep NaN in float columns - - return df
- - -
-[docs] -def convert_dataframe_to_np_structured_array(df: pd.DataFrame): - - df = sanitize_dataframe(df) - # Define the dtype for the structured array, ensuring compatibility with h5py - dtype = [] - for col in df.columns: - - col_data = df[col] - col_dtype = col_data.dtype - - try: - if pd.api.types.is_string_dtype(col_dtype): - # Convert string dtype to fixed-length strings - max_len = col_data.str.len().max() if not col_data.isnull().all() else 0 - dtype.append((col, f'S{max_len}')) - elif pd.api.types.is_integer_dtype(col_dtype): - dtype.append((col, 'i4')) # Assuming 32-bit integer - elif pd.api.types.is_float_dtype(col_dtype): - dtype.append((col, 'f4')) # Assuming 32-bit float - else: - # Handle unsupported data types - print(f"Unsupported dtype found in column '{col}': {col_data.dtype}") - raise ValueError(f"Unsupported data type: {col_data.dtype}") - - except Exception as e: - # Log more detailed error message - print(f"Error processing column '{col}': {e}") - raise - - # Convert the DataFrame to a structured array - structured_array = np.array(list(df.itertuples(index=False, name=None)), dtype=dtype) - - return structured_array
- - -
-[docs] -def convert_string_to_bytes(input_list: list): - """Convert a list of strings into a numpy array with utf8-type entries. - - Parameters - ---------- - input_list (list) : list of string objects - - Returns - ------- - input_array_bytes (ndarray): array of ut8-type entries. - """ - utf8_type = lambda max_length: h5py.string_dtype('utf-8', max_length) - if input_list: - max_length = max(len(item) for item in input_list) - # Convert the strings to bytes with utf-8 encoding, specifying errors='ignore' to skip characters that cannot be encoded - input_list_bytes = [item.encode('utf-8', errors='ignore') for item in input_list] - input_array_bytes = np.array(input_list_bytes,dtype=utf8_type(max_length)) - else: - input_array_bytes = np.array([],dtype=utf8_type(0)) - - return input_array_bytes
- - -
-[docs] -def convert_attrdict_to_np_structured_array(attr_value: dict): - """ - Converts a dictionary of attributes into a numpy structured array for HDF5 - compound type compatibility. - - Each dictionary key is mapped to a field in the structured array, with the - data type (S) determined by the longest string representation of the values. - If the dictionary is empty, the function returns 'missing'. - - Parameters - ---------- - attr_value : dict - Dictionary containing the attributes to be converted. Example: - attr_value = { - 'name': 'Temperature', - 'unit': 'Celsius', - 'value': 23.5, - 'timestamp': '2023-09-26 10:00' - } - - Returns - ------- - new_attr_value : ndarray or str - Numpy structured array with UTF-8 encoded fields. Returns 'missing' if - the input dictionary is empty. - """ - dtype = [] - values_list = [] - max_length = max(len(str(attr_value[key])) for key in attr_value.keys()) - for key in attr_value.keys(): - if key != 'rename_as': - dtype.append((key, f'S{max_length}')) - values_list.append(attr_value[key]) - if values_list: - new_attr_value = np.array([tuple(values_list)], dtype=dtype) - else: - new_attr_value = 'missing' - - return new_attr_value
- - - -
-[docs] -def infer_units(column_name): - # TODO: complete or remove - - match = re.search('\[.+\]') - - if match: - return match - else: - match = re.search('\(.+\)') - - return match
- - -
-[docs] -def progressBar(count_value, total, suffix=''): - bar_length = 100 - filled_up_Length = int(round(bar_length* count_value / float(total))) - percentage = round(100.0 * count_value/float(total),1) - bar = '=' * filled_up_Length + '-' * (bar_length - filled_up_Length) - sys.stdout.write('[%s] %s%s ...%s\r' %(bar, percentage, '%', suffix)) - sys.stdout.flush()
- - -
-[docs] -def copy_directory_with_contraints(input_dir_path, output_dir_path, - select_dir_keywords = None, - select_file_keywords = None, - allowed_file_extensions = None, - dry_run = False): - """ - Copies files from input_dir_path to output_dir_path based on specified constraints. - - Parameters - ---------- - input_dir_path (str): Path to the input directory. - output_dir_path (str): Path to the output directory. - select_dir_keywords (list): optional, List of keywords for selecting directories. - select_file_keywords (list): optional, List of keywords for selecting files. - allowed_file_extensions (list): optional, List of allowed file extensions. - - Returns - ------- - path_to_files_dict (dict): dictionary mapping directory paths to lists of copied file names satisfying the constraints. - """ - - # Unconstrained default behavior: No filters, make sure variable are lists even when defined as None in function signature - select_dir_keywords = select_dir_keywords or [] - select_file_keywords = select_file_keywords or [] - allowed_file_extensions = allowed_file_extensions or [] - - date = created_at('%Y_%m').replace(":", "-") - log_dir='logs/' - setup_logging(log_dir, f"copy_directory_with_contraints_{date}.log") - - # Define helper functions. Return by default true when filtering lists are either None or [] - def has_allowed_extension(filename): - return not allowed_file_extensions or os.path.splitext(filename)[1] in allowed_file_extensions - - def file_is_selected(filename): - return not select_file_keywords or any(keyword in filename for keyword in select_file_keywords) - - - # Collect paths of directories, which are directly connected to the root dir and match select_dir_keywords - paths = [] - if select_dir_keywords: - for item in os.listdir(input_dir_path): #Path(input_dir_path).iterdir(): - if any([item in keyword for keyword in select_dir_keywords]): - paths.append(os.path.join(input_dir_path,item)) - else: - paths.append(input_dir_path) #paths.append(Path(input_dir_path)) - - - path_to_files_dict = {} # Dictionary to store directory-file pairs satisfying constraints - - for subpath in paths: - - for dirpath, _, filenames in os.walk(subpath,topdown=False): - - # Reduce filenames to those that are admissible - admissible_filenames = [ - filename for filename in filenames - if file_is_selected(filename) and has_allowed_extension(filename) - ] - - if admissible_filenames: # Only create directory if there are files to copy - - relative_dirpath = os.path.relpath(dirpath, input_dir_path) - target_dirpath = os.path.join(output_dir_path, relative_dirpath) - path_to_files_dict[target_dirpath] = admissible_filenames - - if not dry_run: - - # Perform the actual copying - - os.makedirs(target_dirpath, exist_ok=True) - - for filename in admissible_filenames: - src_file_path = os.path.join(dirpath, filename) - dest_file_path = os.path.join(target_dirpath, filename) - try: - shutil.copy2(src_file_path, dest_file_path) - except Exception as e: - logging.error("Failed to copy %s: %s", src_file_path, e) - - return path_to_files_dict
- - -
-[docs] -def to_serializable_dtype(value): - - """Transform value's dtype into YAML/JSON compatible dtype - - Parameters - ---------- - value : _type_ - _description_ - - Returns - ------- - _type_ - _description_ - """ - try: - if isinstance(value, np.generic): - if np.issubdtype(value.dtype, np.bytes_): - value = value.decode('utf-8') - elif np.issubdtype(value.dtype, np.unicode_): - value = str(value) - elif np.issubdtype(value.dtype, np.number): - value = float(value) - else: - print('Yaml-compatible data-type was not found. Value has been set to NaN.') - value = np.nan - elif isinstance(value, np.ndarray): - # Handling structured array types (with fields) - if value.dtype.names: - value = {field: to_serializable_dtype(value[field]) for field in value.dtype.names} - else: - # Handling regular array NumPy types with assumption of unform dtype accross array elements - # TODO: evaluate a more general way to check for individual dtypes - if isinstance(value[0], bytes): - # Decode bytes - value = [item.decode('utf-8') for item in value] if len(value) > 1 else value[0].decode('utf-8') - elif isinstance(value[0], str): - # Already a string type - value = [str(item) for item in value] if len(value) > 1 else str(value[0]) - elif isinstance(value[0], int): - # Integer type - value = [int(item) for item in value] if len(value) > 1 else int(value[0]) - elif isinstance(value[0], float): - # Floating type - value = [float(item) for item in value] if len(value) > 1 else float(value[0]) - else: - print('Yaml-compatible data-type was not found. Value has been set to NaN.') - print("Debug: value.dtype is", value.dtype) - value = np.nan - - except Exception as e: - print(f'Error converting value: {e}. Value has been set to NaN.') - value = np.nan - - return value
- - -
-[docs] -def is_structured_array(attr_val): - if isinstance(attr_val,np.ndarray): - return True if attr_val.dtype.names is not None else False - else: - return False
- -
- -
-
- -
-
-
-
- - - + + + + + + utils.g5505_utils — DIMA 1.0.0 documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for utils.g5505_utils

+import pandas as pd
+import os
+import sys
+import shutil
+import datetime
+import logging
+import numpy as np
+import h5py
+import re
+
+
+
+[docs] +def setup_logging(log_dir, log_filename): + """Sets up logging to a specified directory and file. + + Parameters: + log_dir (str): Directory to save the log file. + log_filename (str): Name of the log file. + """ + # Ensure the log directory exists + os.makedirs(log_dir, exist_ok=True) + + # Create a logger instance + logger = logging.getLogger() + logger.setLevel(logging.INFO) + + # Create a file handler + log_path = os.path.join(log_dir, log_filename) + file_handler = logging.FileHandler(log_path) + + # Create a formatter and set it for the handler + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + file_handler.setFormatter(formatter) + + # Add the handler to the logger + logger.addHandler(file_handler)
+ + + +
+[docs] +def is_callable_list(x : list): + return all([callable(item) for item in x])
+ + +
+[docs] +def is_str_list(x : list): + return all([isinstance(item,str) for item in x])
+ + +
+[docs] +def augment_with_filetype(df): + df['filetype'] = [os.path.splitext(item)[1][1::] for item in df['filename']] + #return [os.path.splitext(item)[1][1::] for item in df['filename']] + return df
+ + +
+[docs] +def augment_with_filenumber(df): + df['filenumber'] = [item[0:item.find('_')] for item in df['filename']] + #return [item[0:item.find('_')] for item in df['filename']] + return df
+ + +
+[docs] +def group_by_df_column(df, column_name: str): + """ + df (pandas.DataFrame): + column_name (str): column_name of df by which grouping operation will take place. + """ + + if not column_name in df.columns: + raise ValueError("column_name must be in the columns of df.") + + return df[column_name]
+ + +
+[docs] +def split_sample_col_into_sample_and_data_quality_cols(input_data: pd.DataFrame): + + sample_name = [] + sample_quality = [] + for item in input_data['sample']: + if item.find('(')!=-1: + #print(item) + sample_name.append(item[0:item.find('(')]) + sample_quality.append(item[item.find('(')+1:len(item)-1]) + else: + if item=='': + sample_name.append('Not yet annotated') + sample_quality.append('unevaluated') + else: + sample_name.append(item) + sample_quality.append('good data') + input_data['sample'] = sample_name + input_data['data_quality'] = sample_quality + + return input_data
+ + +
+[docs] +def make_file_copy(source_file_path, output_folder_name : str = 'tmp_files'): + + pathtail, filename = os.path.split(source_file_path) + #backup_filename = 'backup_'+ filename + backup_filename = filename + # Path + ROOT_DIR = os.path.abspath(os.curdir) + + tmp_dirpath = os.path.join(ROOT_DIR,output_folder_name) + if not os.path.exists(tmp_dirpath): + os.mkdir(tmp_dirpath) + + tmp_file_path = os.path.join(tmp_dirpath,backup_filename) + shutil.copy(source_file_path, tmp_file_path) + + return tmp_file_path
+ + +
+[docs] +def created_at(datetime_format = '%Y-%m-%d %H:%M:%S'): + now = datetime.datetime.now() + # Populate now object with time zone information obtained from the local system + now_tz_aware = now.astimezone() + tz = now_tz_aware.strftime('%z') + # Replace colons in the time part of the timestamp with hyphens to make it file name friendly + created_at = now_tz_aware.strftime(datetime_format) #+ '_UTC-OFST_' + tz + return created_at
+ + +
+[docs] +def sanitize_dataframe(df: pd.DataFrame) -> pd.DataFrame: + # Handle datetime columns (convert to string in 'yyyy-mm-dd hh:mm:ss' format) + datetime_cols = df.select_dtypes(include=['datetime']).columns + for col in datetime_cols: + # Convert datetime to string in the specified format, handling NaT + df[col] = df[col].dt.strftime('%Y-%m-%d %H-%M-%S') + + # Handle object columns with mixed types + otype_cols = df.select_dtypes(include='O') + for col in otype_cols: + col_data = df[col] + + # Check if all elements in the column are strings + if col_data.apply(lambda x: isinstance(x, str)).all(): + df[col] = df[col].astype(str) + else: + # If the column contains mixed types, attempt to convert to numeric, coercing errors to NaN + df[col] = pd.to_numeric(col_data, errors='coerce') + + # Handle NaN values differently based on dtype + if pd.api.types.is_string_dtype(df[col]): + # Replace NaN in string columns with empty string + df[col] = df[col].fillna('') # Replace NaN with empty string + elif pd.api.types.is_numeric_dtype(df[col]): + # For numeric columns, we want to keep NaN as it is + # But if integer column has NaN, consider casting to float + if pd.api.types.is_integer_dtype(df[col]): + df[col] = df[col].astype(float) # Cast to float to allow NaN + else: + df[col] = df[col].fillna(np.nan) # Keep NaN in float columns + + return df
+ + +
+[docs] +def convert_dataframe_to_np_structured_array(df: pd.DataFrame): + + df = sanitize_dataframe(df) + # Define the dtype for the structured array, ensuring compatibility with h5py + dtype = [] + for col in df.columns: + + col_data = df[col] + col_dtype = col_data.dtype + + try: + if pd.api.types.is_string_dtype(col_dtype): + # Convert string dtype to fixed-length strings + max_len = col_data.str.len().max() if not col_data.isnull().all() else 0 + dtype.append((col, f'S{max_len}')) + elif pd.api.types.is_integer_dtype(col_dtype): + dtype.append((col, 'i4')) # Assuming 32-bit integer + elif pd.api.types.is_float_dtype(col_dtype): + dtype.append((col, 'f4')) # Assuming 32-bit float + else: + # Handle unsupported data types + print(f"Unsupported dtype found in column '{col}': {col_data.dtype}") + raise ValueError(f"Unsupported data type: {col_data.dtype}") + + except Exception as e: + # Log more detailed error message + print(f"Error processing column '{col}': {e}") + raise + + # Convert the DataFrame to a structured array + structured_array = np.array(list(df.itertuples(index=False, name=None)), dtype=dtype) + + return structured_array
+ + +
+[docs] +def convert_string_to_bytes(input_list: list): + """Convert a list of strings into a numpy array with utf8-type entries. + + Parameters + ---------- + input_list (list) : list of string objects + + Returns + ------- + input_array_bytes (ndarray): array of ut8-type entries. + """ + utf8_type = lambda max_length: h5py.string_dtype('utf-8', max_length) + if input_list: + max_length = max(len(item) for item in input_list) + # Convert the strings to bytes with utf-8 encoding, specifying errors='ignore' to skip characters that cannot be encoded + input_list_bytes = [item.encode('utf-8', errors='ignore') for item in input_list] + input_array_bytes = np.array(input_list_bytes,dtype=utf8_type(max_length)) + else: + input_array_bytes = np.array([],dtype=utf8_type(0)) + + return input_array_bytes
+ + +
+[docs] +def convert_attrdict_to_np_structured_array(attr_value: dict): + """ + Converts a dictionary of attributes into a numpy structured array for HDF5 + compound type compatibility. + + Each dictionary key is mapped to a field in the structured array, with the + data type (S) determined by the longest string representation of the values. + If the dictionary is empty, the function returns 'missing'. + + Parameters + ---------- + attr_value : dict + Dictionary containing the attributes to be converted. Example: + attr_value = { + 'name': 'Temperature', + 'unit': 'Celsius', + 'value': 23.5, + 'timestamp': '2023-09-26 10:00' + } + + Returns + ------- + new_attr_value : ndarray or str + Numpy structured array with UTF-8 encoded fields. Returns 'missing' if + the input dictionary is empty. + """ + dtype = [] + values_list = [] + max_length = max(len(str(attr_value[key])) for key in attr_value.keys()) + for key in attr_value.keys(): + if key != 'rename_as': + dtype.append((key, f'S{max_length}')) + values_list.append(attr_value[key]) + if values_list: + new_attr_value = np.array([tuple(values_list)], dtype=dtype) + else: + new_attr_value = 'missing' + + return new_attr_value
+ + + +
+[docs] +def infer_units(column_name): + # TODO: complete or remove + + match = re.search('\[.+\]') + + if match: + return match + else: + match = re.search('\(.+\)') + + return match
+ + +
+[docs] +def progressBar(count_value, total, suffix=''): + bar_length = 100 + filled_up_Length = int(round(bar_length* count_value / float(total))) + percentage = round(100.0 * count_value/float(total),1) + bar = '=' * filled_up_Length + '-' * (bar_length - filled_up_Length) + sys.stdout.write('[%s] %s%s ...%s\r' %(bar, percentage, '%', suffix)) + sys.stdout.flush()
+ + +
+[docs] +def copy_directory_with_contraints(input_dir_path, output_dir_path, + select_dir_keywords = None, + select_file_keywords = None, + allowed_file_extensions = None, + dry_run = False): + """ + Copies files from input_dir_path to output_dir_path based on specified constraints. + + Parameters + ---------- + input_dir_path (str): Path to the input directory. + output_dir_path (str): Path to the output directory. + select_dir_keywords (list): optional, List of keywords for selecting directories. + select_file_keywords (list): optional, List of keywords for selecting files. + allowed_file_extensions (list): optional, List of allowed file extensions. + + Returns + ------- + path_to_files_dict (dict): dictionary mapping directory paths to lists of copied file names satisfying the constraints. + """ + + # Unconstrained default behavior: No filters, make sure variable are lists even when defined as None in function signature + select_dir_keywords = select_dir_keywords or [] + select_file_keywords = select_file_keywords or [] + allowed_file_extensions = allowed_file_extensions or [] + + date = created_at('%Y_%m').replace(":", "-") + log_dir='logs/' + setup_logging(log_dir, f"copy_directory_with_contraints_{date}.log") + + # Define helper functions. Return by default true when filtering lists are either None or [] + def has_allowed_extension(filename): + return not allowed_file_extensions or os.path.splitext(filename)[1] in allowed_file_extensions + + def file_is_selected(filename): + return not select_file_keywords or any(keyword in filename for keyword in select_file_keywords) + + + # Collect paths of directories, which are directly connected to the root dir and match select_dir_keywords + paths = [] + if select_dir_keywords: + for item in os.listdir(input_dir_path): #Path(input_dir_path).iterdir(): + if any([item in keyword for keyword in select_dir_keywords]): + paths.append(os.path.join(input_dir_path,item)) + else: + paths.append(input_dir_path) #paths.append(Path(input_dir_path)) + + + path_to_files_dict = {} # Dictionary to store directory-file pairs satisfying constraints + + for subpath in paths: + + for dirpath, _, filenames in os.walk(subpath,topdown=False): + + # Reduce filenames to those that are admissible + admissible_filenames = [ + filename for filename in filenames + if file_is_selected(filename) and has_allowed_extension(filename) + ] + + if admissible_filenames: # Only create directory if there are files to copy + + relative_dirpath = os.path.relpath(dirpath, input_dir_path) + target_dirpath = os.path.join(output_dir_path, relative_dirpath) + path_to_files_dict[target_dirpath] = admissible_filenames + + if not dry_run: + + # Perform the actual copying + + os.makedirs(target_dirpath, exist_ok=True) + + for filename in admissible_filenames: + src_file_path = os.path.join(dirpath, filename) + dest_file_path = os.path.join(target_dirpath, filename) + try: + shutil.copy2(src_file_path, dest_file_path) + except Exception as e: + logging.error("Failed to copy %s: %s", src_file_path, e) + + return path_to_files_dict
+ + +
+[docs] +def to_serializable_dtype(value): + + """Transform value's dtype into YAML/JSON compatible dtype + + Parameters + ---------- + value : _type_ + _description_ + + Returns + ------- + _type_ + _description_ + """ + try: + if isinstance(value, np.generic): + if np.issubdtype(value.dtype, np.bytes_): + value = value.decode('utf-8') + elif np.issubdtype(value.dtype, np.unicode_): + value = str(value) + elif np.issubdtype(value.dtype, np.number): + value = float(value) + else: + print('Yaml-compatible data-type was not found. Value has been set to NaN.') + value = np.nan + elif isinstance(value, np.ndarray): + # Handling structured array types (with fields) + if value.dtype.names: + value = {field: to_serializable_dtype(value[field]) for field in value.dtype.names} + else: + # Handling regular array NumPy types with assumption of unform dtype accross array elements + # TODO: evaluate a more general way to check for individual dtypes + if isinstance(value[0], bytes): + # Decode bytes + value = [item.decode('utf-8') for item in value] if len(value) > 1 else value[0].decode('utf-8') + elif isinstance(value[0], str): + # Already a string type + value = [str(item) for item in value] if len(value) > 1 else str(value[0]) + elif isinstance(value[0], int): + # Integer type + value = [int(item) for item in value] if len(value) > 1 else int(value[0]) + elif isinstance(value[0], float): + # Floating type + value = [float(item) for item in value] if len(value) > 1 else float(value[0]) + else: + print('Yaml-compatible data-type was not found. Value has been set to NaN.') + print("Debug: value.dtype is", value.dtype) + value = np.nan + + except Exception as e: + print(f'Error converting value: {e}. Value has been set to NaN.') + value = np.nan + + return value
+ + +
+[docs] +def is_structured_array(attr_val): + if isinstance(attr_val,np.ndarray): + return True if attr_val.dtype.names is not None else False + else: + return False
+ +
+ +
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/docs/build/html/_modules/visualization/hdf5_vis.html b/docs/build/html/_modules/visualization/hdf5_vis.html index 9155ff6..259bc1c 100644 --- a/docs/build/html/_modules/visualization/hdf5_vis.html +++ b/docs/build/html/_modules/visualization/hdf5_vis.html @@ -1,182 +1,182 @@ - - - - - - visualization.hdf5_vis — DIMA 1.0.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -

Source code for visualization.hdf5_vis

-import sys
-import os
-root_dir = os.path.abspath(os.curdir)
-sys.path.append(root_dir)
-
-import h5py
-import yaml
-
-import numpy as np
-import pandas as pd
-
-from plotly.subplots import make_subplots
-import plotly.graph_objects as go
-import plotly.express as px
-#import plotly.io as pio
-from src.hdf5_ops import get_parent_child_relationships
-
- 
-
-
-[docs] -def display_group_hierarchy_on_a_treemap(filename: str): - - """ - filename (str): hdf5 file's filename""" - - with h5py.File(filename,'r') as file: - nodes, parents, values = get_parent_child_relationships(file) - - metadata_list = [] - metadata_dict={} - for key in file.attrs.keys(): - #if 'metadata' in key: - if isinstance(file.attrs[key], str): # Check if the attribute is a string - metadata_key = key[key.find('_') + 1:] - metadata_value = file.attrs[key] - metadata_dict[metadata_key] = metadata_value - metadata_list.append(f'{metadata_key}: {metadata_value}') - - #metadata_dict[key[key.find('_')+1::]]= file.attrs[key] - #metadata_list.append(key[key.find('_')+1::]+':'+file.attrs[key]) - - metadata = '<br>'.join(['<br>'] + metadata_list) - - customdata_series = pd.Series(nodes) - customdata_series[0] = metadata - - fig = make_subplots(1, 1, specs=[[{"type": "domain"}]],) - fig.add_trace(go.Treemap( - labels=nodes, #formating_df['formated_names'][nodes], - parents=parents,#formating_df['formated_names'][parents], - values=values, - branchvalues='remainder', - customdata= customdata_series, - #marker=dict( - # colors=df_all_trees['color'], - # colorscale='RdBu', - # cmid=average_score), - #hovertemplate='<b>%{label} </b> <br> Number of files: %{value}<br> Success rate: %{color:.2f}', - hovertemplate='<b>%{label} </b> <br> Count: %{value} <br> Path: %{customdata}', - name='', - root_color="lightgrey" - )) - fig.update_layout(width = 800, height= 600, margin = dict(t=50, l=25, r=25, b=25)) - fig.show() - file_name, file_ext = os.path.splitext(filename) - fig.write_html(file_name + ".html")
- - - #pio.write_image(fig,file_name + ".png",width=800,height=600,format='png') - -# -
- -
-
- -
-
-
-
- - - + + + + + + visualization.hdf5_vis — DIMA 1.0.0 documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for visualization.hdf5_vis

+import sys
+import os
+root_dir = os.path.abspath(os.curdir)
+sys.path.append(root_dir)
+
+import h5py
+import yaml
+
+import numpy as np
+import pandas as pd
+
+from plotly.subplots import make_subplots
+import plotly.graph_objects as go
+import plotly.express as px
+#import plotly.io as pio
+from src.hdf5_ops import get_parent_child_relationships
+
+ 
+
+
+[docs] +def display_group_hierarchy_on_a_treemap(filename: str): + + """ + filename (str): hdf5 file's filename""" + + with h5py.File(filename,'r') as file: + nodes, parents, values = get_parent_child_relationships(file) + + metadata_list = [] + metadata_dict={} + for key in file.attrs.keys(): + #if 'metadata' in key: + if isinstance(file.attrs[key], str): # Check if the attribute is a string + metadata_key = key[key.find('_') + 1:] + metadata_value = file.attrs[key] + metadata_dict[metadata_key] = metadata_value + metadata_list.append(f'{metadata_key}: {metadata_value}') + + #metadata_dict[key[key.find('_')+1::]]= file.attrs[key] + #metadata_list.append(key[key.find('_')+1::]+':'+file.attrs[key]) + + metadata = '<br>'.join(['<br>'] + metadata_list) + + customdata_series = pd.Series(nodes) + customdata_series[0] = metadata + + fig = make_subplots(1, 1, specs=[[{"type": "domain"}]],) + fig.add_trace(go.Treemap( + labels=nodes, #formating_df['formated_names'][nodes], + parents=parents,#formating_df['formated_names'][parents], + values=values, + branchvalues='remainder', + customdata= customdata_series, + #marker=dict( + # colors=df_all_trees['color'], + # colorscale='RdBu', + # cmid=average_score), + #hovertemplate='<b>%{label} </b> <br> Number of files: %{value}<br> Success rate: %{color:.2f}', + hovertemplate='<b>%{label} </b> <br> Count: %{value} <br> Path: %{customdata}', + name='', + root_color="lightgrey" + )) + fig.update_layout(width = 800, height= 600, margin = dict(t=50, l=25, r=25, b=25)) + fig.show() + file_name, file_ext = os.path.splitext(filename) + fig.write_html(file_name + ".html")
+ + + #pio.write_image(fig,file_name + ".png",width=800,height=600,format='png') + +# +
+ +
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/docs/build/html/_sources/index.rst.txt b/docs/build/html/_sources/index.rst.txt index aadb26a..35afb05 100644 --- a/docs/build/html/_sources/index.rst.txt +++ b/docs/build/html/_sources/index.rst.txt @@ -1,28 +1,28 @@ -.. DIMA documentation master file, created by - sphinx-quickstart on Wed Jul 10 15:50:06 2024. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to DIMA's documentation! -================================ - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - modules/src - - modules/pipelines - - modules/vis - - modules/utils - - modules/notebooks - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` +.. DIMA documentation master file, created by + sphinx-quickstart on Wed Jul 10 15:50:06 2024. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to DIMA's documentation! +================================ + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + modules/src + + modules/pipelines + + modules/vis + + modules/utils + + modules/notebooks + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/build/html/_sources/modules/notebooks.rst.txt b/docs/build/html/_sources/modules/notebooks.rst.txt index d96ad41..c198a44 100644 --- a/docs/build/html/_sources/modules/notebooks.rst.txt +++ b/docs/build/html/_sources/modules/notebooks.rst.txt @@ -1,7 +1,7 @@ -Notebooks -========================== - -.. automodule:: notebooks - :members: - :undoc-members: +Notebooks +========================== + +.. automodule:: notebooks + :members: + :undoc-members: :show-inheritance: \ No newline at end of file diff --git a/docs/build/html/_sources/modules/pipelines.rst.txt b/docs/build/html/_sources/modules/pipelines.rst.txt index 685d139..c28b099 100644 --- a/docs/build/html/_sources/modules/pipelines.rst.txt +++ b/docs/build/html/_sources/modules/pipelines.rst.txt @@ -1,12 +1,12 @@ -Pipelines and workflows -========================== - -.. automodule:: pipelines.data_integration - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: pipelines.metadata_revision - :members: - :undoc-members: +Pipelines and workflows +========================== + +.. automodule:: pipelines.data_integration + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: pipelines.metadata_revision + :members: + :undoc-members: :show-inheritance: \ No newline at end of file diff --git a/docs/build/html/_sources/modules/src.rst.txt b/docs/build/html/_sources/modules/src.rst.txt index ecc8e69..90982ac 100644 --- a/docs/build/html/_sources/modules/src.rst.txt +++ b/docs/build/html/_sources/modules/src.rst.txt @@ -1,18 +1,18 @@ -HDF5 Data Operations -========================== -.. automodule:: src.hdf5_ops - :members: - :undoc-members: - :show-inheritance: - - -HDF5 Writer -========================== - -.. automodule:: src.hdf5_writer - :members: - :undoc-members: - :show-inheritance: - - - +HDF5 Data Operations +========================== +.. automodule:: src.hdf5_ops + :members: + :undoc-members: + :show-inheritance: + + +HDF5 Writer +========================== + +.. automodule:: src.hdf5_writer + :members: + :undoc-members: + :show-inheritance: + + + diff --git a/docs/build/html/_sources/modules/utils.rst.txt b/docs/build/html/_sources/modules/utils.rst.txt index 036a2d3..b34dae9 100644 --- a/docs/build/html/_sources/modules/utils.rst.txt +++ b/docs/build/html/_sources/modules/utils.rst.txt @@ -1,7 +1,7 @@ -Data Structure Conversion -========================= - -.. automodule:: utils.g5505_utils - :members: - :undoc-members: +Data Structure Conversion +========================= + +.. automodule:: utils.g5505_utils + :members: + :undoc-members: :show-inheritance: \ No newline at end of file diff --git a/docs/build/html/_sources/modules/vis.rst.txt b/docs/build/html/_sources/modules/vis.rst.txt index 5a25e0f..98eed89 100644 --- a/docs/build/html/_sources/modules/vis.rst.txt +++ b/docs/build/html/_sources/modules/vis.rst.txt @@ -1,7 +1,7 @@ -Data Visualization -================== - -.. automodule:: visualization.hdf5_vis - :members: - :undoc-members: +Data Visualization +================== + +.. automodule:: visualization.hdf5_vis + :members: + :undoc-members: :show-inheritance: \ No newline at end of file diff --git a/docs/build/html/_sources/notebooks/workflow_di.rst.txt b/docs/build/html/_sources/notebooks/workflow_di.rst.txt index 7920886..385cc5b 100644 --- a/docs/build/html/_sources/notebooks/workflow_di.rst.txt +++ b/docs/build/html/_sources/notebooks/workflow_di.rst.txt @@ -1,7 +1,7 @@ -Tutorial workflows -========================== - -.. automodule:: workflow_data_integration - :members: - :undoc-members: +Tutorial workflows +========================== + +.. automodule:: workflow_data_integration + :members: + :undoc-members: :show-inheritance: \ No newline at end of file diff --git a/docs/build/html/_static/_sphinx_javascript_frameworks_compat.js b/docs/build/html/_static/_sphinx_javascript_frameworks_compat.js index 8141580..b9f8ecd 100644 --- a/docs/build/html/_static/_sphinx_javascript_frameworks_compat.js +++ b/docs/build/html/_static/_sphinx_javascript_frameworks_compat.js @@ -1,123 +1,123 @@ -/* Compatability shim for jQuery and underscores.js. - * - * Copyright Sphinx contributors - * Released under the two clause BSD licence - */ - -/** - * small helper function to urldecode strings - * - * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL - */ -jQuery.urldecode = function(x) { - if (!x) { - return x - } - return decodeURIComponent(x.replace(/\+/g, ' ')); -}; - -/** - * small helper function to urlencode strings - */ -jQuery.urlencode = encodeURIComponent; - -/** - * This function returns the parsed url parameters of the - * current request. Multiple values per key are supported, - * it will always return arrays of strings for the value parts. - */ -jQuery.getQueryParameters = function(s) { - if (typeof s === 'undefined') - s = document.location.search; - var parts = s.substr(s.indexOf('?') + 1).split('&'); - var result = {}; - for (var i = 0; i < parts.length; i++) { - var tmp = parts[i].split('=', 2); - var key = jQuery.urldecode(tmp[0]); - var value = jQuery.urldecode(tmp[1]); - if (key in result) - result[key].push(value); - else - result[key] = [value]; - } - return result; -}; - -/** - * highlight a given string on a jquery object by wrapping it in - * span elements with the given class name. - */ -jQuery.fn.highlightText = function(text, className) { - function highlight(node, addItems) { - if (node.nodeType === 3) { - var val = node.nodeValue; - var pos = val.toLowerCase().indexOf(text); - if (pos >= 0 && - !jQuery(node.parentNode).hasClass(className) && - !jQuery(node.parentNode).hasClass("nohighlight")) { - var span; - var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); - if (isInSVG) { - span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); - } else { - span = document.createElement("span"); - span.className = className; - } - span.appendChild(document.createTextNode(val.substr(pos, text.length))); - node.parentNode.insertBefore(span, node.parentNode.insertBefore( - document.createTextNode(val.substr(pos + text.length)), - node.nextSibling)); - node.nodeValue = val.substr(0, pos); - if (isInSVG) { - var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); - var bbox = node.parentElement.getBBox(); - rect.x.baseVal.value = bbox.x; - rect.y.baseVal.value = bbox.y; - rect.width.baseVal.value = bbox.width; - rect.height.baseVal.value = bbox.height; - rect.setAttribute('class', className); - addItems.push({ - "parent": node.parentNode, - "target": rect}); - } - } - } - else if (!jQuery(node).is("button, select, textarea")) { - jQuery.each(node.childNodes, function() { - highlight(this, addItems); - }); - } - } - var addItems = []; - var result = this.each(function() { - highlight(this, addItems); - }); - for (var i = 0; i < addItems.length; ++i) { - jQuery(addItems[i].parent).before(addItems[i].target); - } - return result; -}; - -/* - * backward compatibility for jQuery.browser - * This will be supported until firefox bug is fixed. - */ -if (!jQuery.browser) { - jQuery.uaMatch = function(ua) { - ua = ua.toLowerCase(); - - var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || - /(webkit)[ \/]([\w.]+)/.exec(ua) || - /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || - /(msie) ([\w.]+)/.exec(ua) || - ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || - []; - - return { - browser: match[ 1 ] || "", - version: match[ 2 ] || "0" - }; - }; - jQuery.browser = {}; - jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; -} +/* Compatability shim for jQuery and underscores.js. + * + * Copyright Sphinx contributors + * Released under the two clause BSD licence + */ + +/** + * small helper function to urldecode strings + * + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL + */ +jQuery.urldecode = function(x) { + if (!x) { + return x + } + return decodeURIComponent(x.replace(/\+/g, ' ')); +}; + +/** + * small helper function to urlencode strings + */ +jQuery.urlencode = encodeURIComponent; + +/** + * This function returns the parsed url parameters of the + * current request. Multiple values per key are supported, + * it will always return arrays of strings for the value parts. + */ +jQuery.getQueryParameters = function(s) { + if (typeof s === 'undefined') + s = document.location.search; + var parts = s.substr(s.indexOf('?') + 1).split('&'); + var result = {}; + for (var i = 0; i < parts.length; i++) { + var tmp = parts[i].split('=', 2); + var key = jQuery.urldecode(tmp[0]); + var value = jQuery.urldecode(tmp[1]); + if (key in result) + result[key].push(value); + else + result[key] = [value]; + } + return result; +}; + +/** + * highlight a given string on a jquery object by wrapping it in + * span elements with the given class name. + */ +jQuery.fn.highlightText = function(text, className) { + function highlight(node, addItems) { + if (node.nodeType === 3) { + var val = node.nodeValue; + var pos = val.toLowerCase().indexOf(text); + if (pos >= 0 && + !jQuery(node.parentNode).hasClass(className) && + !jQuery(node.parentNode).hasClass("nohighlight")) { + var span; + var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.className = className; + } + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + node.parentNode.insertBefore(span, node.parentNode.insertBefore( + document.createTextNode(val.substr(pos + text.length)), + node.nextSibling)); + node.nodeValue = val.substr(0, pos); + if (isInSVG) { + var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); + var bbox = node.parentElement.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute('class', className); + addItems.push({ + "parent": node.parentNode, + "target": rect}); + } + } + } + else if (!jQuery(node).is("button, select, textarea")) { + jQuery.each(node.childNodes, function() { + highlight(this, addItems); + }); + } + } + var addItems = []; + var result = this.each(function() { + highlight(this, addItems); + }); + for (var i = 0; i < addItems.length; ++i) { + jQuery(addItems[i].parent).before(addItems[i].target); + } + return result; +}; + +/* + * backward compatibility for jQuery.browser + * This will be supported until firefox bug is fixed. + */ +if (!jQuery.browser) { + jQuery.uaMatch = function(ua) { + ua = ua.toLowerCase(); + + var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || + /(webkit)[ \/]([\w.]+)/.exec(ua) || + /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || + /(msie) ([\w.]+)/.exec(ua) || + ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || + []; + + return { + browser: match[ 1 ] || "", + version: match[ 2 ] || "0" + }; + }; + jQuery.browser = {}; + jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; +} diff --git a/docs/build/html/_static/basic.css b/docs/build/html/_static/basic.css index f316efc..a07832c 100644 --- a/docs/build/html/_static/basic.css +++ b/docs/build/html/_static/basic.css @@ -1,925 +1,925 @@ -/* - * basic.css - * ~~~~~~~~~ - * - * Sphinx stylesheet -- basic theme. - * - * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -/* -- main layout ----------------------------------------------------------- */ - -div.clearer { - clear: both; -} - -div.section::after { - display: block; - content: ''; - clear: left; -} - -/* -- relbar ---------------------------------------------------------------- */ - -div.related { - width: 100%; - font-size: 90%; -} - -div.related h3 { - display: none; -} - -div.related ul { - margin: 0; - padding: 0 0 0 10px; - list-style: none; -} - -div.related li { - display: inline; -} - -div.related li.right { - float: right; - margin-right: 5px; -} - -/* -- sidebar --------------------------------------------------------------- */ - -div.sphinxsidebarwrapper { - padding: 10px 5px 0 10px; -} - -div.sphinxsidebar { - float: left; - width: 230px; - margin-left: -100%; - font-size: 90%; - word-wrap: break-word; - overflow-wrap : break-word; -} - -div.sphinxsidebar ul { - list-style: none; -} - -div.sphinxsidebar ul ul, -div.sphinxsidebar ul.want-points { - margin-left: 20px; - list-style: square; -} - -div.sphinxsidebar ul ul { - margin-top: 0; - margin-bottom: 0; -} - -div.sphinxsidebar form { - margin-top: 10px; -} - -div.sphinxsidebar input { - border: 1px solid #98dbcc; - font-family: sans-serif; - font-size: 1em; -} - -div.sphinxsidebar #searchbox form.search { - overflow: hidden; -} - -div.sphinxsidebar #searchbox input[type="text"] { - float: left; - width: 80%; - padding: 0.25em; - box-sizing: border-box; -} - -div.sphinxsidebar #searchbox input[type="submit"] { - float: left; - width: 20%; - border-left: none; - padding: 0.25em; - box-sizing: border-box; -} - - -img { - border: 0; - max-width: 100%; -} - -/* -- search page ----------------------------------------------------------- */ - -ul.search { - margin: 10px 0 0 20px; - padding: 0; -} - -ul.search li { - padding: 5px 0 5px 20px; - background-image: url(file.png); - background-repeat: no-repeat; - background-position: 0 7px; -} - -ul.search li a { - font-weight: bold; -} - -ul.search li p.context { - color: #888; - margin: 2px 0 0 30px; - text-align: left; -} - -ul.keywordmatches li.goodmatch a { - font-weight: bold; -} - -/* -- index page ------------------------------------------------------------ */ - -table.contentstable { - width: 90%; - margin-left: auto; - margin-right: auto; -} - -table.contentstable p.biglink { - line-height: 150%; -} - -a.biglink { - font-size: 1.3em; -} - -span.linkdescr { - font-style: italic; - padding-top: 5px; - font-size: 90%; -} - -/* -- general index --------------------------------------------------------- */ - -table.indextable { - width: 100%; -} - -table.indextable td { - text-align: left; - vertical-align: top; -} - -table.indextable ul { - margin-top: 0; - margin-bottom: 0; - list-style-type: none; -} - -table.indextable > tbody > tr > td > ul { - padding-left: 0em; -} - -table.indextable tr.pcap { - height: 10px; -} - -table.indextable tr.cap { - margin-top: 10px; - background-color: #f2f2f2; -} - -img.toggler { - margin-right: 3px; - margin-top: 3px; - cursor: pointer; -} - -div.modindex-jumpbox { - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - margin: 1em 0 1em 0; - padding: 0.4em; -} - -div.genindex-jumpbox { - border-top: 1px solid #ddd; - border-bottom: 1px solid #ddd; - margin: 1em 0 1em 0; - padding: 0.4em; -} - -/* -- domain module index --------------------------------------------------- */ - -table.modindextable td { - padding: 2px; - border-collapse: collapse; -} - -/* -- general body styles --------------------------------------------------- */ - -div.body { - min-width: 360px; - max-width: 800px; -} - -div.body p, div.body dd, div.body li, div.body blockquote { - -moz-hyphens: auto; - -ms-hyphens: auto; - -webkit-hyphens: auto; - hyphens: auto; -} - -a.headerlink { - visibility: hidden; -} - -a:visited { - color: #551A8B; -} - -h1:hover > a.headerlink, -h2:hover > a.headerlink, -h3:hover > a.headerlink, -h4:hover > a.headerlink, -h5:hover > a.headerlink, -h6:hover > a.headerlink, -dt:hover > a.headerlink, -caption:hover > a.headerlink, -p.caption:hover > a.headerlink, -div.code-block-caption:hover > a.headerlink { - visibility: visible; -} - -div.body p.caption { - text-align: inherit; -} - -div.body td { - text-align: left; -} - -.first { - margin-top: 0 !important; -} - -p.rubric { - margin-top: 30px; - font-weight: bold; -} - -img.align-left, figure.align-left, .figure.align-left, object.align-left { - clear: left; - float: left; - margin-right: 1em; -} - -img.align-right, figure.align-right, .figure.align-right, object.align-right { - clear: right; - float: right; - margin-left: 1em; -} - -img.align-center, figure.align-center, .figure.align-center, object.align-center { - display: block; - margin-left: auto; - margin-right: auto; -} - -img.align-default, figure.align-default, .figure.align-default { - display: block; - margin-left: auto; - margin-right: auto; -} - -.align-left { - text-align: left; -} - -.align-center { - text-align: center; -} - -.align-default { - text-align: center; -} - -.align-right { - text-align: right; -} - -/* -- sidebars -------------------------------------------------------------- */ - -div.sidebar, -aside.sidebar { - margin: 0 0 0.5em 1em; - border: 1px solid #ddb; - padding: 7px; - background-color: #ffe; - width: 40%; - float: right; - clear: right; - overflow-x: auto; -} - -p.sidebar-title { - font-weight: bold; -} - -nav.contents, -aside.topic, -div.admonition, div.topic, blockquote { - clear: left; -} - -/* -- topics ---------------------------------------------------------------- */ - -nav.contents, -aside.topic, -div.topic { - border: 1px solid #ccc; - padding: 7px; - margin: 10px 0 10px 0; -} - -p.topic-title { - font-size: 1.1em; - font-weight: bold; - margin-top: 10px; -} - -/* -- admonitions ----------------------------------------------------------- */ - -div.admonition { - margin-top: 10px; - margin-bottom: 10px; - padding: 7px; -} - -div.admonition dt { - font-weight: bold; -} - -p.admonition-title { - margin: 0px 10px 5px 0px; - font-weight: bold; -} - -div.body p.centered { - text-align: center; - margin-top: 25px; -} - -/* -- content of sidebars/topics/admonitions -------------------------------- */ - -div.sidebar > :last-child, -aside.sidebar > :last-child, -nav.contents > :last-child, -aside.topic > :last-child, -div.topic > :last-child, -div.admonition > :last-child { - margin-bottom: 0; -} - -div.sidebar::after, -aside.sidebar::after, -nav.contents::after, -aside.topic::after, -div.topic::after, -div.admonition::after, -blockquote::after { - display: block; - content: ''; - clear: both; -} - -/* -- tables ---------------------------------------------------------------- */ - -table.docutils { - margin-top: 10px; - margin-bottom: 10px; - border: 0; - border-collapse: collapse; -} - -table.align-center { - margin-left: auto; - margin-right: auto; -} - -table.align-default { - margin-left: auto; - margin-right: auto; -} - -table caption span.caption-number { - font-style: italic; -} - -table caption span.caption-text { -} - -table.docutils td, table.docutils th { - padding: 1px 8px 1px 5px; - border-top: 0; - border-left: 0; - border-right: 0; - border-bottom: 1px solid #aaa; -} - -th { - text-align: left; - padding-right: 5px; -} - -table.citation { - border-left: solid 1px gray; - margin-left: 1px; -} - -table.citation td { - border-bottom: none; -} - -th > :first-child, -td > :first-child { - margin-top: 0px; -} - -th > :last-child, -td > :last-child { - margin-bottom: 0px; -} - -/* -- figures --------------------------------------------------------------- */ - -div.figure, figure { - margin: 0.5em; - padding: 0.5em; -} - -div.figure p.caption, figcaption { - padding: 0.3em; -} - -div.figure p.caption span.caption-number, -figcaption span.caption-number { - font-style: italic; -} - -div.figure p.caption span.caption-text, -figcaption span.caption-text { -} - -/* -- field list styles ----------------------------------------------------- */ - -table.field-list td, table.field-list th { - border: 0 !important; -} - -.field-list ul { - margin: 0; - padding-left: 1em; -} - -.field-list p { - margin: 0; -} - -.field-name { - -moz-hyphens: manual; - -ms-hyphens: manual; - -webkit-hyphens: manual; - hyphens: manual; -} - -/* -- hlist styles ---------------------------------------------------------- */ - -table.hlist { - margin: 1em 0; -} - -table.hlist td { - vertical-align: top; -} - -/* -- object description styles --------------------------------------------- */ - -.sig { - font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; -} - -.sig-name, code.descname { - background-color: transparent; - font-weight: bold; -} - -.sig-name { - font-size: 1.1em; -} - -code.descname { - font-size: 1.2em; -} - -.sig-prename, code.descclassname { - background-color: transparent; -} - -.optional { - font-size: 1.3em; -} - -.sig-paren { - font-size: larger; -} - -.sig-param.n { - font-style: italic; -} - -/* C++ specific styling */ - -.sig-inline.c-texpr, -.sig-inline.cpp-texpr { - font-family: unset; -} - -.sig.c .k, .sig.c .kt, -.sig.cpp .k, .sig.cpp .kt { - color: #0033B3; -} - -.sig.c .m, -.sig.cpp .m { - color: #1750EB; -} - -.sig.c .s, .sig.c .sc, -.sig.cpp .s, .sig.cpp .sc { - color: #067D17; -} - - -/* -- other body styles ----------------------------------------------------- */ - -ol.arabic { - list-style: decimal; -} - -ol.loweralpha { - list-style: lower-alpha; -} - -ol.upperalpha { - list-style: upper-alpha; -} - -ol.lowerroman { - list-style: lower-roman; -} - -ol.upperroman { - list-style: upper-roman; -} - -:not(li) > ol > li:first-child > :first-child, -:not(li) > ul > li:first-child > :first-child { - margin-top: 0px; -} - -:not(li) > ol > li:last-child > :last-child, -:not(li) > ul > li:last-child > :last-child { - margin-bottom: 0px; -} - -ol.simple ol p, -ol.simple ul p, -ul.simple ol p, -ul.simple ul p { - margin-top: 0; -} - -ol.simple > li:not(:first-child) > p, -ul.simple > li:not(:first-child) > p { - margin-top: 0; -} - -ol.simple p, -ul.simple p { - margin-bottom: 0; -} - -aside.footnote > span, -div.citation > span { - float: left; -} -aside.footnote > span:last-of-type, -div.citation > span:last-of-type { - padding-right: 0.5em; -} -aside.footnote > p { - margin-left: 2em; -} -div.citation > p { - margin-left: 4em; -} -aside.footnote > p:last-of-type, -div.citation > p:last-of-type { - margin-bottom: 0em; -} -aside.footnote > p:last-of-type:after, -div.citation > p:last-of-type:after { - content: ""; - clear: both; -} - -dl.field-list { - display: grid; - grid-template-columns: fit-content(30%) auto; -} - -dl.field-list > dt { - font-weight: bold; - word-break: break-word; - padding-left: 0.5em; - padding-right: 5px; -} - -dl.field-list > dd { - padding-left: 0.5em; - margin-top: 0em; - margin-left: 0em; - margin-bottom: 0em; -} - -dl { - margin-bottom: 15px; -} - -dd > :first-child { - margin-top: 0px; -} - -dd ul, dd table { - margin-bottom: 10px; -} - -dd { - margin-top: 3px; - margin-bottom: 10px; - margin-left: 30px; -} - -.sig dd { - margin-top: 0px; - margin-bottom: 0px; -} - -.sig dl { - margin-top: 0px; - margin-bottom: 0px; -} - -dl > dd:last-child, -dl > dd:last-child > :last-child { - margin-bottom: 0; -} - -dt:target, span.highlighted { - background-color: #fbe54e; -} - -rect.highlighted { - fill: #fbe54e; -} - -dl.glossary dt { - font-weight: bold; - font-size: 1.1em; -} - -.versionmodified { - font-style: italic; -} - -.system-message { - background-color: #fda; - padding: 5px; - border: 3px solid red; -} - -.footnote:target { - background-color: #ffa; -} - -.line-block { - display: block; - margin-top: 1em; - margin-bottom: 1em; -} - -.line-block .line-block { - margin-top: 0; - margin-bottom: 0; - margin-left: 1.5em; -} - -.guilabel, .menuselection { - font-family: sans-serif; -} - -.accelerator { - text-decoration: underline; -} - -.classifier { - font-style: oblique; -} - -.classifier:before { - font-style: normal; - margin: 0 0.5em; - content: ":"; - display: inline-block; -} - -abbr, acronym { - border-bottom: dotted 1px; - cursor: help; -} - -.translated { - background-color: rgba(207, 255, 207, 0.2) -} - -.untranslated { - background-color: rgba(255, 207, 207, 0.2) -} - -/* -- code displays --------------------------------------------------------- */ - -pre { - overflow: auto; - overflow-y: hidden; /* fixes display issues on Chrome browsers */ -} - -pre, div[class*="highlight-"] { - clear: both; -} - -span.pre { - -moz-hyphens: none; - -ms-hyphens: none; - -webkit-hyphens: none; - hyphens: none; - white-space: nowrap; -} - -div[class*="highlight-"] { - margin: 1em 0; -} - -td.linenos pre { - border: 0; - background-color: transparent; - color: #aaa; -} - -table.highlighttable { - display: block; -} - -table.highlighttable tbody { - display: block; -} - -table.highlighttable tr { - display: flex; -} - -table.highlighttable td { - margin: 0; - padding: 0; -} - -table.highlighttable td.linenos { - padding-right: 0.5em; -} - -table.highlighttable td.code { - flex: 1; - overflow: hidden; -} - -.highlight .hll { - display: block; -} - -div.highlight pre, -table.highlighttable pre { - margin: 0; -} - -div.code-block-caption + div { - margin-top: 0; -} - -div.code-block-caption { - margin-top: 1em; - padding: 2px 5px; - font-size: small; -} - -div.code-block-caption code { - background-color: transparent; -} - -table.highlighttable td.linenos, -span.linenos, -div.highlight span.gp { /* gp: Generic.Prompt */ - user-select: none; - -webkit-user-select: text; /* Safari fallback only */ - -webkit-user-select: none; /* Chrome/Safari */ - -moz-user-select: none; /* Firefox */ - -ms-user-select: none; /* IE10+ */ -} - -div.code-block-caption span.caption-number { - padding: 0.1em 0.3em; - font-style: italic; -} - -div.code-block-caption span.caption-text { -} - -div.literal-block-wrapper { - margin: 1em 0; -} - -code.xref, a code { - background-color: transparent; - font-weight: bold; -} - -h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { - background-color: transparent; -} - -.viewcode-link { - float: right; -} - -.viewcode-back { - float: right; - font-family: sans-serif; -} - -div.viewcode-block:target { - margin: -1px -10px; - padding: 0 10px; -} - -/* -- math display ---------------------------------------------------------- */ - -img.math { - vertical-align: middle; -} - -div.body div.math p { - text-align: center; -} - -span.eqno { - float: right; -} - -span.eqno a.headerlink { - position: absolute; - z-index: 1; -} - -div.math:hover a.headerlink { - visibility: visible; -} - -/* -- printout stylesheet --------------------------------------------------- */ - -@media print { - div.document, - div.documentwrapper, - div.bodywrapper { - margin: 0 !important; - width: 100%; - } - - div.sphinxsidebar, - div.related, - div.footer, - #top-link { - display: none; - } +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 360px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +a:visited { + color: #551A8B; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +nav.contents, +aside.topic, +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +nav.contents, +aside.topic, +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +.sig dd { + margin-top: 0px; + margin-bottom: 0px; +} + +.sig dl { + margin-top: 0px; + margin-bottom: 0px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +.translated { + background-color: rgba(207, 255, 207, 0.2) +} + +.untranslated { + background-color: rgba(255, 207, 207, 0.2) +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } } \ No newline at end of file diff --git a/docs/build/html/_static/css/fonts/fontawesome-webfont.svg b/docs/build/html/_static/css/fonts/fontawesome-webfont.svg index 855c845..d7534c9 100644 --- a/docs/build/html/_static/css/fonts/fontawesome-webfont.svg +++ b/docs/build/html/_static/css/fonts/fontawesome-webfont.svg @@ -1,2671 +1,2671 @@ - - - - -Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 - By ,,, -Copyright Dave Gandy 2016. All rights reserved. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/build/html/_static/css/theme.css b/docs/build/html/_static/css/theme.css index 19a446a..219adcf 100644 --- a/docs/build/html/_static/css/theme.css +++ b/docs/build/html/_static/css/theme.css @@ -1,4 +1,4 @@ -html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden],audio:not([controls]){display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;text-decoration:none}ins,mark{color:#000}mark{background:#ff0;font-style:italic;font-weight:700}.rst-content code,.rst-content tt,code,kbd,pre,samp{font-family:monospace,serif;_font-family:courier new,monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:after,q:before{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,ol,ul{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure,form{margin:0}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none!important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{body,html,section{background:none!important}*{box-shadow:none!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}.rst-content .toctree-wrapper>p.caption,h2,h3,p{orphans:3;widows:3}.rst-content .toctree-wrapper>p.caption,h2,h3{page-break-after:avoid}}.btn,.fa:before,.icon:before,.rst-content .admonition,.rst-content .admonition-title:before,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .code-block-caption .headerlink:before,.rst-content .danger,.rst-content .eqno .headerlink:before,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-alert,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before,input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}/*! - * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) +html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden],audio:not([controls]){display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;text-decoration:none}ins,mark{color:#000}mark{background:#ff0;font-style:italic;font-weight:700}.rst-content code,.rst-content tt,code,kbd,pre,samp{font-family:monospace,serif;_font-family:courier new,monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:after,q:before{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,ol,ul{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure,form{margin:0}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none!important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{body,html,section{background:none!important}*{box-shadow:none!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}.rst-content .toctree-wrapper>p.caption,h2,h3,p{orphans:3;widows:3}.rst-content .toctree-wrapper>p.caption,h2,h3{page-break-after:avoid}}.btn,.fa:before,.icon:before,.rst-content .admonition,.rst-content .admonition-title:before,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .code-block-caption .headerlink:before,.rst-content .danger,.rst-content .eqno .headerlink:before,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-alert,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before,input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa-pull-left.icon,.fa.fa-pull-left,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content .eqno .fa-pull-left.headerlink,.rst-content .fa-pull-left.admonition-title,.rst-content code.download span.fa-pull-left:first-child,.rst-content dl dt .fa-pull-left.headerlink,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content p .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.wy-menu-vertical li.current>a button.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-left.toctree-expand,.wy-menu-vertical li button.fa-pull-left.toctree-expand{margin-right:.3em}.fa-pull-right.icon,.fa.fa-pull-right,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content .eqno .fa-pull-right.headerlink,.rst-content .fa-pull-right.admonition-title,.rst-content code.download span.fa-pull-right:first-child,.rst-content dl dt .fa-pull-right.headerlink,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content p .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.wy-menu-vertical li.current>a button.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-right.toctree-expand,.wy-menu-vertical li button.fa-pull-right.toctree-expand{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.pull-left.icon,.rst-content .code-block-caption .pull-left.headerlink,.rst-content .eqno .pull-left.headerlink,.rst-content .pull-left.admonition-title,.rst-content code.download span.pull-left:first-child,.rst-content dl dt .pull-left.headerlink,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content p .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.wy-menu-vertical li.current>a button.pull-left.toctree-expand,.wy-menu-vertical li.on a button.pull-left.toctree-expand,.wy-menu-vertical li button.pull-left.toctree-expand{margin-right:.3em}.fa.pull-right,.pull-right.icon,.rst-content .code-block-caption .pull-right.headerlink,.rst-content .eqno .pull-right.headerlink,.rst-content .pull-right.admonition-title,.rst-content code.download span.pull-right:first-child,.rst-content dl dt .pull-right.headerlink,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content p .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.wy-menu-vertical li.current>a button.pull-right.toctree-expand,.wy-menu-vertical li.on a button.pull-right.toctree-expand,.wy-menu-vertical li button.pull-right.toctree-expand{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);-ms-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);-ms-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.rst-content .admonition-title:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.icon-caret-down:before,.wy-dropdown .caret:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li button.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:""}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-sign-language:before,.fa-signing:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-address-card:before,.fa-vcard:before{content:""}.fa-address-card-o:before,.fa-vcard-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{font-family:inherit}.fa:before,.icon:before,.rst-content .admonition-title:before,.rst-content .code-block-caption .headerlink:before,.rst-content .eqno .headerlink:before,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before{font-family:FontAwesome;display:inline-block;font-style:normal;font-weight:400;line-height:1;text-decoration:inherit}.rst-content .code-block-caption a .headerlink,.rst-content .eqno a .headerlink,.rst-content a .admonition-title,.rst-content code.download a span:first-child,.rst-content dl dt a .headerlink,.rst-content h1 a .headerlink,.rst-content h2 a .headerlink,.rst-content h3 a .headerlink,.rst-content h4 a .headerlink,.rst-content h5 a .headerlink,.rst-content h6 a .headerlink,.rst-content p.caption a .headerlink,.rst-content p a .headerlink,.rst-content table>caption a .headerlink,.rst-content tt.download a span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li a button.toctree-expand,a .fa,a .icon,a .rst-content .admonition-title,a .rst-content .code-block-caption .headerlink,a .rst-content .eqno .headerlink,a .rst-content code.download span:first-child,a .rst-content dl dt .headerlink,a .rst-content h1 .headerlink,a .rst-content h2 .headerlink,a .rst-content h3 .headerlink,a .rst-content h4 .headerlink,a .rst-content h5 .headerlink,a .rst-content h6 .headerlink,a .rst-content p.caption .headerlink,a .rst-content p .headerlink,a .rst-content table>caption .headerlink,a .rst-content tt.download span:first-child,a .wy-menu-vertical li button.toctree-expand{display:inline-block;text-decoration:inherit}.btn .fa,.btn .icon,.btn .rst-content .admonition-title,.btn .rst-content .code-block-caption .headerlink,.btn .rst-content .eqno .headerlink,.btn .rst-content code.download span:first-child,.btn .rst-content dl dt .headerlink,.btn .rst-content h1 .headerlink,.btn .rst-content h2 .headerlink,.btn .rst-content h3 .headerlink,.btn .rst-content h4 .headerlink,.btn .rst-content h5 .headerlink,.btn .rst-content h6 .headerlink,.btn .rst-content p .headerlink,.btn .rst-content table>caption .headerlink,.btn .rst-content tt.download span:first-child,.btn .wy-menu-vertical li.current>a button.toctree-expand,.btn .wy-menu-vertical li.on a button.toctree-expand,.btn .wy-menu-vertical li button.toctree-expand,.nav .fa,.nav .icon,.nav .rst-content .admonition-title,.nav .rst-content .code-block-caption .headerlink,.nav .rst-content .eqno .headerlink,.nav .rst-content code.download span:first-child,.nav .rst-content dl dt .headerlink,.nav .rst-content h1 .headerlink,.nav .rst-content h2 .headerlink,.nav .rst-content h3 .headerlink,.nav .rst-content h4 .headerlink,.nav .rst-content h5 .headerlink,.nav .rst-content h6 .headerlink,.nav .rst-content p .headerlink,.nav .rst-content table>caption .headerlink,.nav .rst-content tt.download span:first-child,.nav .wy-menu-vertical li.current>a button.toctree-expand,.nav .wy-menu-vertical li.on a button.toctree-expand,.nav .wy-menu-vertical li button.toctree-expand,.rst-content .btn .admonition-title,.rst-content .code-block-caption .btn .headerlink,.rst-content .code-block-caption .nav .headerlink,.rst-content .eqno .btn .headerlink,.rst-content .eqno .nav .headerlink,.rst-content .nav .admonition-title,.rst-content code.download .btn span:first-child,.rst-content code.download .nav span:first-child,.rst-content dl dt .btn .headerlink,.rst-content dl dt .nav .headerlink,.rst-content h1 .btn .headerlink,.rst-content h1 .nav .headerlink,.rst-content h2 .btn .headerlink,.rst-content h2 .nav .headerlink,.rst-content h3 .btn .headerlink,.rst-content h3 .nav .headerlink,.rst-content h4 .btn .headerlink,.rst-content h4 .nav .headerlink,.rst-content h5 .btn .headerlink,.rst-content h5 .nav .headerlink,.rst-content h6 .btn .headerlink,.rst-content h6 .nav .headerlink,.rst-content p .btn .headerlink,.rst-content p .nav .headerlink,.rst-content table>caption .btn .headerlink,.rst-content table>caption .nav .headerlink,.rst-content tt.download .btn span:first-child,.rst-content tt.download .nav span:first-child,.wy-menu-vertical li .btn button.toctree-expand,.wy-menu-vertical li.current>a .btn button.toctree-expand,.wy-menu-vertical li.current>a .nav button.toctree-expand,.wy-menu-vertical li .nav button.toctree-expand,.wy-menu-vertical li.on a .btn button.toctree-expand,.wy-menu-vertical li.on a .nav button.toctree-expand{display:inline}.btn .fa-large.icon,.btn .fa.fa-large,.btn .rst-content .code-block-caption .fa-large.headerlink,.btn .rst-content .eqno .fa-large.headerlink,.btn .rst-content .fa-large.admonition-title,.btn .rst-content code.download span.fa-large:first-child,.btn .rst-content dl dt .fa-large.headerlink,.btn .rst-content h1 .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.btn .rst-content p .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.btn .wy-menu-vertical li button.fa-large.toctree-expand,.nav .fa-large.icon,.nav .fa.fa-large,.nav .rst-content .code-block-caption .fa-large.headerlink,.nav .rst-content .eqno .fa-large.headerlink,.nav .rst-content .fa-large.admonition-title,.nav .rst-content code.download span.fa-large:first-child,.nav .rst-content dl dt .fa-large.headerlink,.nav .rst-content h1 .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.nav .rst-content p .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.nav .wy-menu-vertical li button.fa-large.toctree-expand,.rst-content .btn .fa-large.admonition-title,.rst-content .code-block-caption .btn .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.rst-content .eqno .btn .fa-large.headerlink,.rst-content .eqno .nav .fa-large.headerlink,.rst-content .nav .fa-large.admonition-title,.rst-content code.download .btn span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.rst-content dl dt .btn .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.rst-content p .btn .fa-large.headerlink,.rst-content p .nav .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.rst-content tt.download .btn span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.wy-menu-vertical li .btn button.fa-large.toctree-expand,.wy-menu-vertical li .nav button.fa-large.toctree-expand{line-height:.9em}.btn .fa-spin.icon,.btn .fa.fa-spin,.btn .rst-content .code-block-caption .fa-spin.headerlink,.btn .rst-content .eqno .fa-spin.headerlink,.btn .rst-content .fa-spin.admonition-title,.btn .rst-content code.download span.fa-spin:first-child,.btn .rst-content dl dt .fa-spin.headerlink,.btn .rst-content h1 .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.btn .rst-content p .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.btn .wy-menu-vertical li button.fa-spin.toctree-expand,.nav .fa-spin.icon,.nav .fa.fa-spin,.nav .rst-content .code-block-caption .fa-spin.headerlink,.nav .rst-content .eqno .fa-spin.headerlink,.nav .rst-content .fa-spin.admonition-title,.nav .rst-content code.download span.fa-spin:first-child,.nav .rst-content dl dt .fa-spin.headerlink,.nav .rst-content h1 .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.nav .rst-content p .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.nav .wy-menu-vertical li button.fa-spin.toctree-expand,.rst-content .btn .fa-spin.admonition-title,.rst-content .code-block-caption .btn .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.rst-content .eqno .btn .fa-spin.headerlink,.rst-content .eqno .nav .fa-spin.headerlink,.rst-content .nav .fa-spin.admonition-title,.rst-content code.download .btn span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.rst-content dl dt .btn .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.rst-content p .btn .fa-spin.headerlink,.rst-content p .nav .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.rst-content tt.download .btn span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.wy-menu-vertical li .btn button.fa-spin.toctree-expand,.wy-menu-vertical li .nav button.fa-spin.toctree-expand{display:inline-block}.btn.fa:before,.btn.icon:before,.rst-content .btn.admonition-title:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content .eqno .btn.headerlink:before,.rst-content code.download span.btn:first-child:before,.rst-content dl dt .btn.headerlink:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content p .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.wy-menu-vertical li button.btn.toctree-expand:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.btn.icon:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content .eqno .btn.headerlink:hover:before,.rst-content code.download span.btn:first-child:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content p .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.wy-menu-vertical li button.btn.toctree-expand:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .icon:before,.btn-mini .rst-content .admonition-title:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.btn-mini .rst-content .eqno .headerlink:before,.btn-mini .rst-content code.download span:first-child:before,.btn-mini .rst-content dl dt .headerlink:before,.btn-mini .rst-content h1 .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.btn-mini .rst-content p .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.btn-mini .wy-menu-vertical li button.toctree-expand:before,.rst-content .btn-mini .admonition-title:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.rst-content .eqno .btn-mini .headerlink:before,.rst-content code.download .btn-mini span:first-child:before,.rst-content dl dt .btn-mini .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.rst-content p .btn-mini .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.rst-content tt.download .btn-mini span:first-child:before,.wy-menu-vertical li .btn-mini button.toctree-expand:before{font-size:14px;vertical-align:-15%}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.wy-alert{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.rst-content .admonition-title,.wy-alert-title{font-weight:700;display:block;color:#fff;background:#6ab0de;padding:6px 12px;margin:-12px -12px 12px}.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.admonition,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.wy-alert.wy-alert-danger{background:#fdf3f2}.rst-content .danger .admonition-title,.rst-content .danger .wy-alert-title,.rst-content .error .admonition-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .admonition-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.wy-alert.wy-alert-danger .wy-alert-title{background:#f29f97}.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .warning,.rst-content .wy-alert-warning.admonition,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.note,.rst-content .wy-alert-warning.seealso,.rst-content .wy-alert-warning.tip,.wy-alert.wy-alert-warning{background:#ffedcc}.rst-content .admonition-todo .admonition-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .attention .admonition-title,.rst-content .attention .wy-alert-title,.rst-content .caution .admonition-title,.rst-content .caution .wy-alert-title,.rst-content .warning .admonition-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.admonition .admonition-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.wy-alert.wy-alert-warning .wy-alert-title{background:#f0b37e}.rst-content .note,.rst-content .seealso,.rst-content .wy-alert-info.admonition,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.wy-alert.wy-alert-info{background:#e7f2fa}.rst-content .note .admonition-title,.rst-content .note .wy-alert-title,.rst-content .seealso .admonition-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .admonition-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.wy-alert.wy-alert-info .wy-alert-title{background:#6ab0de}.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.admonition,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.warning,.wy-alert.wy-alert-success{background:#dbfaf4}.rst-content .hint .admonition-title,.rst-content .hint .wy-alert-title,.rst-content .important .admonition-title,.rst-content .important .wy-alert-title,.rst-content .tip .admonition-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .admonition-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.wy-alert.wy-alert-success .wy-alert-title{background:#1abc9c}.rst-content .wy-alert-neutral.admonition,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.wy-alert.wy-alert-neutral{background:#f3f6f6}.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .admonition-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.wy-alert.wy-alert-neutral .wy-alert-title{color:#404040;background:#e1e4e5}.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.wy-alert.wy-alert-neutral a{color:#2980b9}.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .note p:last-child,.rst-content .seealso p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.wy-alert p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width:768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px;color:#fff;border:1px solid rgba(0,0,0,.1);background-color:#27ae60;text-decoration:none;font-weight:400;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 2px -1px hsla(0,0%,100%,.5),inset 0 -2px 0 0 rgba(0,0,0,.1);outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:inset 0 -1px 0 0 rgba(0,0,0,.05),inset 0 2px 0 0 rgba(0,0,0,.1);padding:8px 12px 6px}.btn:visited{color:#fff}.btn-disabled,.btn-disabled:active,.btn-disabled:focus,.btn-disabled:hover,.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9!important}.btn-info:hover{background-color:#2e8ece!important}.btn-neutral{background-color:#f3f6f6!important;color:#404040!important}.btn-neutral:hover{background-color:#e5ebeb!important;color:#404040}.btn-neutral:visited{color:#404040!important}.btn-success{background-color:#27ae60!important}.btn-success:hover{background-color:#295!important}.btn-danger{background-color:#e74c3c!important}.btn-danger:hover{background-color:#ea6153!important}.btn-warning{background-color:#e67e22!important}.btn-warning:hover{background-color:#e98b39!important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f!important}.btn-link{background-color:transparent!important;color:#2980b9;box-shadow:none;border-color:transparent!important}.btn-link:active,.btn-link:hover{background-color:transparent!important;color:#409ad5!important;box-shadow:none}.btn-link:visited{color:#9b59b6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:after,.wy-btn-group:before{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:1px solid #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:1px solid #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type=search]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned .wy-help-inline,.wy-form-aligned input,.wy-form-aligned label,.wy-form-aligned select,.wy-form-aligned textarea{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{margin:0}fieldset,legend{border:0;padding:0}legend{width:100%;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label,legend{display:block}label{margin:0 0 .3125em;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;max-width:1200px;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:after,.wy-control-group:before{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#e74c3c}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full input[type=color],.wy-control-group .wy-form-full input[type=date],.wy-control-group .wy-form-full input[type=datetime-local],.wy-control-group .wy-form-full input[type=datetime],.wy-control-group .wy-form-full input[type=email],.wy-control-group .wy-form-full input[type=month],.wy-control-group .wy-form-full input[type=number],.wy-control-group .wy-form-full input[type=password],.wy-control-group .wy-form-full input[type=search],.wy-control-group .wy-form-full input[type=tel],.wy-control-group .wy-form-full input[type=text],.wy-control-group .wy-form-full input[type=time],.wy-control-group .wy-form-full input[type=url],.wy-control-group .wy-form-full input[type=week],.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves input[type=color],.wy-control-group .wy-form-halves input[type=date],.wy-control-group .wy-form-halves input[type=datetime-local],.wy-control-group .wy-form-halves input[type=datetime],.wy-control-group .wy-form-halves input[type=email],.wy-control-group .wy-form-halves input[type=month],.wy-control-group .wy-form-halves input[type=number],.wy-control-group .wy-form-halves input[type=password],.wy-control-group .wy-form-halves input[type=search],.wy-control-group .wy-form-halves input[type=tel],.wy-control-group .wy-form-halves input[type=text],.wy-control-group .wy-form-halves input[type=time],.wy-control-group .wy-form-halves input[type=url],.wy-control-group .wy-form-halves input[type=week],.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds input[type=color],.wy-control-group .wy-form-thirds input[type=date],.wy-control-group .wy-form-thirds input[type=datetime-local],.wy-control-group .wy-form-thirds input[type=datetime],.wy-control-group .wy-form-thirds input[type=email],.wy-control-group .wy-form-thirds input[type=month],.wy-control-group .wy-form-thirds input[type=number],.wy-control-group .wy-form-thirds input[type=password],.wy-control-group .wy-form-thirds input[type=search],.wy-control-group .wy-form-thirds input[type=tel],.wy-control-group .wy-form-thirds input[type=text],.wy-control-group .wy-form-thirds input[type=time],.wy-control-group .wy-form-thirds input[type=url],.wy-control-group .wy-form-thirds input[type=week],.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full{float:left;display:block;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child,.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(odd){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child,.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control,.wy-control-no-input{margin:6px 0 0;font-size:90%}.wy-control-no-input{display:inline-block}.wy-control-group.fluid-input input[type=color],.wy-control-group.fluid-input input[type=date],.wy-control-group.fluid-input input[type=datetime-local],.wy-control-group.fluid-input input[type=datetime],.wy-control-group.fluid-input input[type=email],.wy-control-group.fluid-input input[type=month],.wy-control-group.fluid-input input[type=number],.wy-control-group.fluid-input input[type=password],.wy-control-group.fluid-input input[type=search],.wy-control-group.fluid-input input[type=tel],.wy-control-group.fluid-input input[type=text],.wy-control-group.fluid-input input[type=time],.wy-control-group.fluid-input input[type=url],.wy-control-group.fluid-input input[type=week]{width:100%}.wy-form-message-inline{padding-left:.3em;color:#666;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;*overflow:visible}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type=datetime-local]{padding:.34375em .625em}input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type=checkbox],input[type=radio],input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus{outline:0;outline:thin dotted\9;border-color:#333}input.no-focus:focus{border-color:#ccc!important}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,select:focus:invalid:focus,textarea:focus:invalid:focus{border-color:#e74c3c}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#e74c3c}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}input[readonly],select[disabled],select[readonly],textarea[disabled],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type=checkbox][disabled],input[type=radio][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:1px solid #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{left:0;top:0;width:36px;height:12px;background:#ccc}.wy-switch:after,.wy-switch:before{position:absolute;content:"";display:block;border-radius:4px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{width:18px;height:18px;background:#999;left:-3px;top:-3px}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27ae60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type=color],.wy-control-group.wy-control-group-error input[type=date],.wy-control-group.wy-control-group-error input[type=datetime-local],.wy-control-group.wy-control-group-error input[type=datetime],.wy-control-group.wy-control-group-error input[type=email],.wy-control-group.wy-control-group-error input[type=month],.wy-control-group.wy-control-group-error input[type=number],.wy-control-group.wy-control-group-error input[type=password],.wy-control-group.wy-control-group-error input[type=search],.wy-control-group.wy-control-group-error input[type=tel],.wy-control-group.wy-control-group-error input[type=text],.wy-control-group.wy-control-group-error input[type=time],.wy-control-group.wy-control-group-error input[type=url],.wy-control-group.wy-control-group-error input[type=week],.wy-control-group.wy-control-group-error textarea{border:1px solid #e74c3c}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width:480px){.wy-form button[type=submit]{margin:.7em 0 0}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=text],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week],.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0}.wy-form-message,.wy-form-message-inline,.wy-form .wy-help-inline{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width:768px){.tablet-hide{display:none}}@media screen and (max-width:480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.rst-content table.docutils,.rst-content table.field-list,.wy-table{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.rst-content table.docutils caption,.rst-content table.field-list caption,.wy-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.rst-content table.docutils td,.rst-content table.docutils th,.rst-content table.field-list td,.rst-content table.field-list th,.wy-table td,.wy-table th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.rst-content table.docutils td:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list td:first-child,.rst-content table.field-list th:first-child,.wy-table td:first-child,.wy-table th:first-child{border-left-width:0}.rst-content table.docutils thead,.rst-content table.field-list thead,.wy-table thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.rst-content table.docutils thead th,.rst-content table.field-list thead th,.wy-table thead th{font-weight:700;border-bottom:2px solid #e1e4e5}.rst-content table.docutils td,.rst-content table.field-list td,.wy-table td{background-color:transparent;vertical-align:middle}.rst-content table.docutils td p,.rst-content table.field-list td p,.wy-table td p{line-height:18px}.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child,.wy-table td p:last-child{margin-bottom:0}.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min,.wy-table .wy-table-cell-min{width:1%;padding-right:0}.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:grey;font-size:90%}.wy-table-tertiary{color:grey;font-size:80%}.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td,.wy-table-backed,.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td{background-color:#f3f6f6}.rst-content table.docutils,.wy-table-bordered-all{border:1px solid #e1e4e5}.rst-content table.docutils td,.wy-table-bordered-all td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.rst-content table.docutils tbody>tr:last-child td,.wy-table-bordered-all tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0!important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980b9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9b59b6}html{height:100%}body,html{overflow-x:hidden}body{font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-weight:400;color:#404040;min-height:100%;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22!important}a.wy-text-warning:hover{color:#eb9950!important}.wy-text-info{color:#2980b9!important}a.wy-text-info:hover{color:#409ad5!important}.wy-text-success{color:#27ae60!important}a.wy-text-success:hover{color:#36d278!important}.wy-text-danger{color:#e74c3c!important}a.wy-text-danger:hover{color:#ed7669!important}.wy-text-neutral{color:#404040!important}a.wy-text-neutral:hover{color:#595959!important}.rst-content .toctree-wrapper>p.caption,h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif}p{line-height:24px;font-size:16px;margin:0 0 24px}h1{font-size:175%}.rst-content .toctree-wrapper>p.caption,h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}.rst-content code,.rst-content tt,code{white-space:nowrap;max-width:100%;background:#fff;border:1px solid #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#e74c3c;overflow-x:auto}.rst-content tt.code-large,code.code-large{font-size:90%}.rst-content .section ul,.rst-content .toctree-wrapper ul,.rst-content section ul,.wy-plain-list-disc,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.rst-content .section ul li,.rst-content .toctree-wrapper ul li,.rst-content section ul li,.wy-plain-list-disc li,article ul li{list-style:disc;margin-left:24px}.rst-content .section ul li p:last-child,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li p:last-child,.rst-content .toctree-wrapper ul li ul,.rst-content section ul li p:last-child,.rst-content section ul li ul,.wy-plain-list-disc li p:last-child,.wy-plain-list-disc li ul,article ul li p:last-child,article ul li ul{margin-bottom:0}.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,.rst-content section ul li li,.wy-plain-list-disc li li,article ul li li{list-style:circle}.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,.rst-content section ul li li li,.wy-plain-list-disc li li li,article ul li li li{list-style:square}.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,.rst-content section ul li ol li,.wy-plain-list-disc li ol li,article ul li ol li{list-style:decimal}.rst-content .section ol,.rst-content .section ol.arabic,.rst-content .toctree-wrapper ol,.rst-content .toctree-wrapper ol.arabic,.rst-content section ol,.rst-content section ol.arabic,.wy-plain-list-decimal,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.rst-content .section ol.arabic li,.rst-content .section ol li,.rst-content .toctree-wrapper ol.arabic li,.rst-content .toctree-wrapper ol li,.rst-content section ol.arabic li,.rst-content section ol li,.wy-plain-list-decimal li,article ol li{list-style:decimal;margin-left:24px}.rst-content .section ol.arabic li ul,.rst-content .section ol li p:last-child,.rst-content .section ol li ul,.rst-content .toctree-wrapper ol.arabic li ul,.rst-content .toctree-wrapper ol li p:last-child,.rst-content .toctree-wrapper ol li ul,.rst-content section ol.arabic li ul,.rst-content section ol li p:last-child,.rst-content section ol li ul,.wy-plain-list-decimal li p:last-child,.wy-plain-list-decimal li ul,article ol li p:last-child,article ol li ul{margin-bottom:0}.rst-content .section ol.arabic li ul li,.rst-content .section ol li ul li,.rst-content .toctree-wrapper ol.arabic li ul li,.rst-content .toctree-wrapper ol li ul li,.rst-content section ol.arabic li ul li,.rst-content section ol li ul li,.wy-plain-list-decimal li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:after,.wy-breadcrumbs:before{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs>li{display:inline-block;padding-top:5px}.wy-breadcrumbs>li.wy-breadcrumbs-aside{float:right}.rst-content .wy-breadcrumbs>li code,.rst-content .wy-breadcrumbs>li tt,.wy-breadcrumbs>li .rst-content tt,.wy-breadcrumbs>li code{all:inherit;color:inherit}.breadcrumb-item:before{content:"/";color:#bbb;font-size:13px;padding:0 6px 0 3px}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width:480px){.wy-breadcrumbs-extra,.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:after,.wy-menu-horiz:before{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz li,.wy-menu-horiz ul{display:inline-block}.wy-menu-horiz li:hover{background:hsla(0,0%,100%,.1)}.wy-menu-horiz li.divide-left{border-left:1px solid #404040}.wy-menu-horiz li.divide-right{border-right:1px solid #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#55a5d9;height:32px;line-height:32px;padding:0 1.618em;margin:12px 0 0;display:block;font-weight:700;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:1px solid #404040}.wy-menu-vertical li.divide-bottom{border-bottom:1px solid #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:grey;border-right:1px solid #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.rst-content .wy-menu-vertical li tt,.wy-menu-vertical li .rst-content tt,.wy-menu-vertical li code{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li button.toctree-expand{display:block;float:left;margin-left:-1.2em;line-height:18px;color:#4d4d4d;border:none;background:none;padding:0}.wy-menu-vertical li.current>a,.wy-menu-vertical li.on a{color:#404040;font-weight:700;position:relative;background:#fcfcfc;border:none;padding:.4045em 1.618em}.wy-menu-vertical li.current>a:hover,.wy-menu-vertical li.on a:hover{background:#fcfcfc}.wy-menu-vertical li.current>a:hover button.toctree-expand,.wy-menu-vertical li.on a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand{display:block;line-height:18px;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:1px solid #c9c9c9;border-top:1px solid #c9c9c9}.wy-menu-vertical .toctree-l1.current .toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .toctree-l11>ul{display:none}.wy-menu-vertical .toctree-l1.current .current.toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .current.toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .current.toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .current.toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .current.toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .current.toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .current.toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .current.toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .current.toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .current.toctree-l11>ul{display:block}.wy-menu-vertical li.toctree-l3,.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a,.wy-menu-vertical li.toctree-l5 a,.wy-menu-vertical li.toctree-l6 a,.wy-menu-vertical li.toctree-l7 a,.wy-menu-vertical li.toctree-l8 a,.wy-menu-vertical li.toctree-l9 a,.wy-menu-vertical li.toctree-l10 a{color:#404040}.wy-menu-vertical li.toctree-l2 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l3 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l4 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l5 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l6 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l7 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l8 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l9 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l10 a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{display:block}.wy-menu-vertical li.toctree-l2.current>a{padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{padding:.4045em 1.618em .4045em 4.045em}.wy-menu-vertical li.toctree-l3.current>a{padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{padding:.4045em 1.618em .4045em 5.663em}.wy-menu-vertical li.toctree-l4.current>a{padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a{padding:.4045em 1.618em .4045em 7.281em}.wy-menu-vertical li.toctree-l5.current>a{padding:.4045em 7.281em}.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a{padding:.4045em 1.618em .4045em 8.899em}.wy-menu-vertical li.toctree-l6.current>a{padding:.4045em 8.899em}.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a{padding:.4045em 1.618em .4045em 10.517em}.wy-menu-vertical li.toctree-l7.current>a{padding:.4045em 10.517em}.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a{padding:.4045em 1.618em .4045em 12.135em}.wy-menu-vertical li.toctree-l8.current>a{padding:.4045em 12.135em}.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a{padding:.4045em 1.618em .4045em 13.753em}.wy-menu-vertical li.toctree-l9.current>a{padding:.4045em 13.753em}.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a{padding:.4045em 1.618em .4045em 15.371em}.wy-menu-vertical li.toctree-l10.current>a{padding:.4045em 15.371em}.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{padding:.4045em 1.618em .4045em 16.989em}.wy-menu-vertical li.toctree-l2.current>a,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{background:#c9c9c9}.wy-menu-vertical li.toctree-l2 button.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3.current>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{background:#bdbdbd}.wy-menu-vertical li.toctree-l3 button.toctree-expand{color:#969696}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:400}.wy-menu-vertical a{line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover button.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-menu-vertical a:active button.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980b9;text-align:center;color:#fcfcfc}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a{color:#fcfcfc;font-size:100%;font-weight:700;display:inline-block;padding:4px 6px;margin-bottom:.809em;max-width:100%}.wy-side-nav-search .wy-dropdown>a:hover,.wy-side-nav-search>a:hover{background:hsla(0,0%,100%,.1)}.wy-side-nav-search .wy-dropdown>a img.logo,.wy-side-nav-search>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search .wy-dropdown>a.icon img.logo,.wy-side-nav-search>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.version{margin-top:-.4045em;margin-bottom:.809em;font-weight:400;color:hsla(0,0%,100%,.3)}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:after,.wy-nav-top:before{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:700}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:grey}footer p{margin-bottom:12px}.rst-content footer span.commit tt,footer span.commit .rst-content tt,footer span.commit code{padding:0;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:1em;background:none;border:none;color:grey}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:after,.rst-footer-buttons:before{width:100%;display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:after,.rst-breadcrumbs-buttons:before{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:1px solid #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:1px solid #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:grey;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width:768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-menu.wy-menu-vertical,.wy-side-nav-search,.wy-side-scroll{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width:1100px){.wy-nav-content-wrap{background:rgba(0,0,0,.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,.wy-nav-side,footer{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:after,.rst-versions .rst-current-version:before{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-content .eqno .rst-versions .rst-current-version .headerlink,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-content p .rst-versions .rst-current-version .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-versions .rst-current-version .rst-content .eqno .headerlink,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-versions .rst-current-version .rst-content p .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-versions .rst-current-version .wy-menu-vertical li button.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version button.toctree-expand{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}.rst-content .toctree-wrapper>p.caption,.rst-content h1,.rst-content h2,.rst-content h3,.rst-content h4,.rst-content h5,.rst-content h6{margin-bottom:24px}.rst-content img{max-width:100%;height:auto}.rst-content div.figure,.rst-content figure{margin-bottom:24px}.rst-content div.figure .caption-text,.rst-content figure .caption-text{font-style:italic}.rst-content div.figure p:last-child.caption,.rst-content figure p:last-child.caption{margin-bottom:0}.rst-content div.figure.align-center,.rst-content figure.align-center{text-align:center}.rst-content .section>a>img,.rst-content .section>img,.rst-content section>a>img,.rst-content section>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"\f08e";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;display:block;overflow:auto}.rst-content div[class^=highlight],.rst-content pre.literal-block{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px}.rst-content div[class^=highlight] div[class^=highlight],.rst-content pre.literal-block div[class^=highlight]{padding:0;border:none;margin:0}.rst-content div[class^=highlight] td.code{width:100%}.rst-content .linenodiv pre{border-right:1px solid #e6e9ea;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^=highlight] pre{white-space:pre;margin:0;padding:12px;display:block;overflow:auto}.rst-content div[class^=highlight] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content .linenodiv pre,.rst-content div[class^=highlight] pre,.rst-content pre.literal-block{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:12px;line-height:1.4}.rst-content div.highlight .gp,.rst-content div.highlight span.linenos{user-select:none;pointer-events:none}.rst-content div.highlight span.linenos{display:inline-block;padding-left:0;padding-right:12px;margin-right:12px;border-right:1px solid #e6e9ea}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^=highlight],.rst-content div[class^=highlight] pre{white-space:pre-wrap}}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning{clear:both}.rst-content .admonition-todo .last,.rst-content .admonition-todo>:last-child,.rst-content .admonition .last,.rst-content .admonition>:last-child,.rst-content .attention .last,.rst-content .attention>:last-child,.rst-content .caution .last,.rst-content .caution>:last-child,.rst-content .danger .last,.rst-content .danger>:last-child,.rst-content .error .last,.rst-content .error>:last-child,.rst-content .hint .last,.rst-content .hint>:last-child,.rst-content .important .last,.rst-content .important>:last-child,.rst-content .note .last,.rst-content .note>:last-child,.rst-content .seealso .last,.rst-content .seealso>:last-child,.rst-content .tip .last,.rst-content .tip>:last-child,.rst-content .warning .last,.rst-content .warning>:last-child{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent!important;border-color:rgba(0,0,0,.1)!important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha>li,.rst-content .toctree-wrapper ol.loweralpha,.rst-content .toctree-wrapper ol.loweralpha>li,.rst-content section ol.loweralpha,.rst-content section ol.loweralpha>li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha>li,.rst-content .toctree-wrapper ol.upperalpha,.rst-content .toctree-wrapper ol.upperalpha>li,.rst-content section ol.upperalpha,.rst-content section ol.upperalpha>li{list-style:upper-alpha}.rst-content .section ol li>*,.rst-content .section ul li>*,.rst-content .toctree-wrapper ol li>*,.rst-content .toctree-wrapper ul li>*,.rst-content section ol li>*,.rst-content section ul li>*{margin-top:12px;margin-bottom:12px}.rst-content .section ol li>:first-child,.rst-content .section ul li>:first-child,.rst-content .toctree-wrapper ol li>:first-child,.rst-content .toctree-wrapper ul li>:first-child,.rst-content section ol li>:first-child,.rst-content section ul li>:first-child{margin-top:0}.rst-content .section ol li>p,.rst-content .section ol li>p:last-child,.rst-content .section ul li>p,.rst-content .section ul li>p:last-child,.rst-content .toctree-wrapper ol li>p,.rst-content .toctree-wrapper ol li>p:last-child,.rst-content .toctree-wrapper ul li>p,.rst-content .toctree-wrapper ul li>p:last-child,.rst-content section ol li>p,.rst-content section ol li>p:last-child,.rst-content section ul li>p,.rst-content section ul li>p:last-child{margin-bottom:12px}.rst-content .section ol li>p:only-child,.rst-content .section ol li>p:only-child:last-child,.rst-content .section ul li>p:only-child,.rst-content .section ul li>p:only-child:last-child,.rst-content .toctree-wrapper ol li>p:only-child,.rst-content .toctree-wrapper ol li>p:only-child:last-child,.rst-content .toctree-wrapper ul li>p:only-child,.rst-content .toctree-wrapper ul li>p:only-child:last-child,.rst-content section ol li>p:only-child,.rst-content section ol li>p:only-child:last-child,.rst-content section ul li>p:only-child,.rst-content section ul li>p:only-child:last-child{margin-bottom:0}.rst-content .section ol li>ol,.rst-content .section ol li>ul,.rst-content .section ul li>ol,.rst-content .section ul li>ul,.rst-content .toctree-wrapper ol li>ol,.rst-content .toctree-wrapper ol li>ul,.rst-content .toctree-wrapper ul li>ol,.rst-content .toctree-wrapper ul li>ul,.rst-content section ol li>ol,.rst-content section ol li>ul,.rst-content section ul li>ol,.rst-content section ul li>ul{margin-bottom:12px}.rst-content .section ol.simple li>*,.rst-content .section ol.simple li ol,.rst-content .section ol.simple li ul,.rst-content .section ul.simple li>*,.rst-content .section ul.simple li ol,.rst-content .section ul.simple li ul,.rst-content .toctree-wrapper ol.simple li>*,.rst-content .toctree-wrapper ol.simple li ol,.rst-content .toctree-wrapper ol.simple li ul,.rst-content .toctree-wrapper ul.simple li>*,.rst-content .toctree-wrapper ul.simple li ol,.rst-content .toctree-wrapper ul.simple li ul,.rst-content section ol.simple li>*,.rst-content section ol.simple li ol,.rst-content section ol.simple li ul,.rst-content section ul.simple li>*,.rst-content section ul.simple li ol,.rst-content section ul.simple li ul{margin-top:0;margin-bottom:0}.rst-content .line-block{margin-left:0;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0}.rst-content .topic-title{font-weight:700;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0 0 24px 24px}.rst-content .align-left{float:left;margin:0 24px 24px 0}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink{opacity:0;font-size:14px;font-family:FontAwesome;margin-left:.5em}.rst-content .code-block-caption .headerlink:focus,.rst-content .code-block-caption:hover .headerlink,.rst-content .eqno .headerlink:focus,.rst-content .eqno:hover .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink:focus,.rst-content .toctree-wrapper>p.caption:hover .headerlink,.rst-content dl dt .headerlink:focus,.rst-content dl dt:hover .headerlink,.rst-content h1 .headerlink:focus,.rst-content h1:hover .headerlink,.rst-content h2 .headerlink:focus,.rst-content h2:hover .headerlink,.rst-content h3 .headerlink:focus,.rst-content h3:hover .headerlink,.rst-content h4 .headerlink:focus,.rst-content h4:hover .headerlink,.rst-content h5 .headerlink:focus,.rst-content h5:hover .headerlink,.rst-content h6 .headerlink:focus,.rst-content h6:hover .headerlink,.rst-content p.caption .headerlink:focus,.rst-content p.caption:hover .headerlink,.rst-content p .headerlink:focus,.rst-content p:hover .headerlink,.rst-content table>caption .headerlink:focus,.rst-content table>caption:hover .headerlink{opacity:1}.rst-content p a{overflow-wrap:anywhere}.rst-content .wy-table td p,.rst-content .wy-table td ul,.rst-content .wy-table th p,.rst-content .wy-table th ul,.rst-content table.docutils td p,.rst-content table.docutils td ul,.rst-content table.docutils th p,.rst-content table.docutils th ul,.rst-content table.field-list td p,.rst-content table.field-list td ul,.rst-content table.field-list th p,.rst-content table.field-list th ul{font-size:inherit}.rst-content .btn:focus{outline:2px solid}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:1px solid #e1e4e5}.rst-content .sidebar dl,.rst-content .sidebar p,.rst-content .sidebar ul{font-size:90%}.rst-content .sidebar .last,.rst-content .sidebar>:last-child{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif;font-weight:700;background:#e1e4e5;padding:6px 12px;margin:-24px -24px 24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;box-shadow:0 0 0 2px #f1c40f;display:inline;font-weight:700}.rst-content .citation-reference,.rst-content .footnote-reference{vertical-align:baseline;position:relative;top:-.4em;line-height:0;font-size:90%}.rst-content .citation-reference>span.fn-bracket,.rst-content .footnote-reference>span.fn-bracket{display:none}.rst-content .hlist{width:100%}.rst-content dl dt span.classifier:before{content:" : "}.rst-content dl dt span.classifier-delimiter{display:none!important}html.writer-html4 .rst-content table.docutils.citation,html.writer-html4 .rst-content table.docutils.footnote{background:none;border:none}html.writer-html4 .rst-content table.docutils.citation td,html.writer-html4 .rst-content table.docutils.citation tr,html.writer-html4 .rst-content table.docutils.footnote td,html.writer-html4 .rst-content table.docutils.footnote tr{border:none;background-color:transparent!important;white-space:normal}html.writer-html4 .rst-content table.docutils.citation td.label,html.writer-html4 .rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:auto minmax(80%,95%)}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{display:inline-grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{display:grid;grid-template-columns:auto auto minmax(.65rem,auto) minmax(40%,95%)}html.writer-html5 .rst-content aside.citation>span.label,html.writer-html5 .rst-content aside.footnote>span.label,html.writer-html5 .rst-content div.citation>span.label{grid-column-start:1;grid-column-end:2}html.writer-html5 .rst-content aside.citation>span.backrefs,html.writer-html5 .rst-content aside.footnote>span.backrefs,html.writer-html5 .rst-content div.citation>span.backrefs{grid-column-start:2;grid-column-end:3;grid-row-start:1;grid-row-end:3}html.writer-html5 .rst-content aside.citation>p,html.writer-html5 .rst-content aside.footnote>p,html.writer-html5 .rst-content div.citation>p{grid-column-start:4;grid-column-end:5}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{margin-bottom:24px}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{padding-left:1rem}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dd,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dd,html.writer-html5 .rst-content dl.footnote>dt{margin-bottom:0}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.footnote>dt{margin:0 .5rem .5rem 0;line-height:1.2rem;word-break:break-all;font-weight:400}html.writer-html5 .rst-content dl.citation>dt>span.brackets:before,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.citation>dt>span.brackets:after,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a{word-break:keep-all}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a:not(:first-child):before,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.citation>dd p,html.writer-html5 .rst-content dl.footnote>dd p{font-size:.9rem}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{padding-left:1rem;padding-right:1rem;font-size:.9rem;line-height:1.2rem}html.writer-html5 .rst-content aside.citation p,html.writer-html5 .rst-content aside.footnote p,html.writer-html5 .rst-content div.citation p{font-size:.9rem;line-height:1.2rem;margin-bottom:12px}html.writer-html5 .rst-content aside.citation span.backrefs,html.writer-html5 .rst-content aside.footnote span.backrefs,html.writer-html5 .rst-content div.citation span.backrefs{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content aside.citation span.backrefs>a,html.writer-html5 .rst-content aside.footnote span.backrefs>a,html.writer-html5 .rst-content div.citation span.backrefs>a{word-break:keep-all}html.writer-html5 .rst-content aside.citation span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content aside.footnote span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content div.citation span.backrefs>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content aside.citation span.label,html.writer-html5 .rst-content aside.footnote span.label,html.writer-html5 .rst-content div.citation span.label{line-height:1.2rem}html.writer-html5 .rst-content aside.citation-list,html.writer-html5 .rst-content aside.footnote-list,html.writer-html5 .rst-content div.citation-list{margin-bottom:24px}html.writer-html5 .rst-content dl.option-list kbd{font-size:.9rem}.rst-content table.docutils.footnote,html.writer-html4 .rst-content table.docutils.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content aside.footnote-list aside.footnote,html.writer-html5 .rst-content div.citation-list>div.citation,html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{color:grey}.rst-content table.docutils.footnote code,.rst-content table.docutils.footnote tt,html.writer-html4 .rst-content table.docutils.citation code,html.writer-html4 .rst-content table.docutils.citation tt,html.writer-html5 .rst-content aside.footnote-list aside.footnote code,html.writer-html5 .rst-content aside.footnote-list aside.footnote tt,html.writer-html5 .rst-content aside.footnote code,html.writer-html5 .rst-content aside.footnote tt,html.writer-html5 .rst-content div.citation-list>div.citation code,html.writer-html5 .rst-content div.citation-list>div.citation tt,html.writer-html5 .rst-content dl.citation code,html.writer-html5 .rst-content dl.citation tt,html.writer-html5 .rst-content dl.footnote code,html.writer-html5 .rst-content dl.footnote tt{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}html.writer-html5 .rst-content table.docutils th{border:1px solid #e1e4e5}html.writer-html5 .rst-content table.docutils td>p,html.writer-html5 .rst-content table.docutils th>p{line-height:1rem;margin-bottom:0;font-size:.9rem}.rst-content table.docutils td .last,.rst-content table.docutils td .last>:last-child{margin-bottom:0}.rst-content table.field-list,.rst-content table.field-list td{border:none}.rst-content table.field-list td p{line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content code,.rst-content tt{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;padding:2px 5px}.rst-content code big,.rst-content code em,.rst-content tt big,.rst-content tt em{font-size:100%!important;line-height:normal}.rst-content code.literal,.rst-content tt.literal{color:#e74c3c;white-space:normal}.rst-content code.xref,.rst-content tt.xref,a .rst-content code,a .rst-content tt{font-weight:700;color:#404040;overflow-wrap:normal}.rst-content kbd,.rst-content pre,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace}.rst-content a code,.rst-content a tt{color:#2980b9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:700;margin-bottom:12px}.rst-content dl ol,.rst-content dl p,.rst-content dl table,.rst-content dl ul{margin-bottom:12px}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}.rst-content dl dd>ol:last-child,.rst-content dl dd>p:last-child,.rst-content dl dd>table:last-child,.rst-content dl dd>ul:last-child{margin-bottom:0}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple){margin-bottom:24px}html.writer-html4 .rst-content dl:not(.docutils)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:3px solid #6ab0de;padding:6px;position:relative}html.writer-html4 .rst-content dl:not(.docutils)>dt:before,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:before{color:#6ab0de}html.writer-html4 .rst-content dl:not(.docutils)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{margin-bottom:6px;border:none;border-left:3px solid #ccc;background:#f0f0f0;color:#555}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils)>dt:first-child,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:first-child{margin-top:0}html.writer-html4 .rst-content dl:not(.docutils) code.descclassname,html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{background-color:transparent;border:none;padding:0;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .optional,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .property,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .property{display:inline-block;padding-right:8px;max-width:100%}html.writer-html4 .rst-content dl:not(.docutils) .k,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .k{font-style:italic}html.writer-html4 .rst-content dl:not(.docutils) .descclassname,html.writer-html4 .rst-content dl:not(.docutils) .descname,html.writer-html4 .rst-content dl:not(.docutils) .sig-name,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .sig-name{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#000}.rst-content .viewcode-back,.rst-content .viewcode-link{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:700}.rst-content code.download,.rst-content tt.download{background:inherit;padding:inherit;font-weight:400;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content code.download span:first-child,.rst-content tt.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{margin-right:4px}.rst-content .guilabel,.rst-content .menuselection{font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content .guilabel,.rst-content .menuselection{border:1px solid #7fbbe3;background:#e7f2fa}.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>.kbd,.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>kbd{color:inherit;font-size:80%;background-color:#fff;border:1px solid #a6a6a6;border-radius:4px;box-shadow:0 2px grey;padding:2.4px 6px;margin:auto 0}.rst-content .versionmodified{font-style:italic}@media screen and (max-width:480px){.rst-content .sidebar{width:100%}}span[id*=MathJax-Span]{color:#404040}.math{text-align:center}@font-face{font-family:Lato;src:url(fonts/lato-normal.woff2?bd03a2cc277bbbc338d464e679fe9942) format("woff2"),url(fonts/lato-normal.woff?27bd77b9162d388cb8d4c4217c7c5e2a) format("woff");font-weight:400;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold.woff2?cccb897485813c7c256901dbca54ecf2) format("woff2"),url(fonts/lato-bold.woff?d878b6c29b10beca227e9eef4246111b) format("woff");font-weight:700;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold-italic.woff2?0b6bb6725576b072c5d0b02ecdd1900d) format("woff2"),url(fonts/lato-bold-italic.woff?9c7e4e9eb485b4a121c760e61bc3707c) format("woff");font-weight:700;font-style:italic;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-normal-italic.woff2?4eb103b4d12be57cb1d040ed5e162e9d) format("woff2"),url(fonts/lato-normal-italic.woff?f28f2d6482446544ef1ea1ccc6dd5892) format("woff");font-weight:400;font-style:italic;font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:400;src:url(fonts/Roboto-Slab-Regular.woff2?7abf5b8d04d26a2cafea937019bca958) format("woff2"),url(fonts/Roboto-Slab-Regular.woff?c1be9284088d487c5e3ff0a10a92e58c) format("woff");font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:700;src:url(fonts/Roboto-Slab-Bold.woff2?9984f4a9bda09be08e83f2506954adbe) format("woff2"),url(fonts/Roboto-Slab-Bold.woff?bed5564a116b05148e3b3bea6fb1162a) format("woff");font-display:block} \ No newline at end of file diff --git a/docs/build/html/_static/doctools.js b/docs/build/html/_static/doctools.js index 4d67807..1e004aa 100644 --- a/docs/build/html/_static/doctools.js +++ b/docs/build/html/_static/doctools.js @@ -1,156 +1,156 @@ -/* - * doctools.js - * ~~~~~~~~~~~ - * - * Base JavaScript utilities for all Sphinx HTML documentation. - * - * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ -"use strict"; - -const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ - "TEXTAREA", - "INPUT", - "SELECT", - "BUTTON", -]); - -const _ready = (callback) => { - if (document.readyState !== "loading") { - callback(); - } else { - document.addEventListener("DOMContentLoaded", callback); - } -}; - -/** - * Small JavaScript module for the documentation. - */ -const Documentation = { - init: () => { - Documentation.initDomainIndexTable(); - Documentation.initOnKeyListeners(); - }, - - /** - * i18n support - */ - TRANSLATIONS: {}, - PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), - LOCALE: "unknown", - - // gettext and ngettext don't access this so that the functions - // can safely bound to a different name (_ = Documentation.gettext) - gettext: (string) => { - const translated = Documentation.TRANSLATIONS[string]; - switch (typeof translated) { - case "undefined": - return string; // no translation - case "string": - return translated; // translation exists - default: - return translated[0]; // (singular, plural) translation tuple exists - } - }, - - ngettext: (singular, plural, n) => { - const translated = Documentation.TRANSLATIONS[singular]; - if (typeof translated !== "undefined") - return translated[Documentation.PLURAL_EXPR(n)]; - return n === 1 ? singular : plural; - }, - - addTranslations: (catalog) => { - Object.assign(Documentation.TRANSLATIONS, catalog.messages); - Documentation.PLURAL_EXPR = new Function( - "n", - `return (${catalog.plural_expr})` - ); - Documentation.LOCALE = catalog.locale; - }, - - /** - * helper function to focus on search bar - */ - focusSearchBar: () => { - document.querySelectorAll("input[name=q]")[0]?.focus(); - }, - - /** - * Initialise the domain index toggle buttons - */ - initDomainIndexTable: () => { - const toggler = (el) => { - const idNumber = el.id.substr(7); - const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); - if (el.src.substr(-9) === "minus.png") { - el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; - toggledRows.forEach((el) => (el.style.display = "none")); - } else { - el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; - toggledRows.forEach((el) => (el.style.display = "")); - } - }; - - const togglerElements = document.querySelectorAll("img.toggler"); - togglerElements.forEach((el) => - el.addEventListener("click", (event) => toggler(event.currentTarget)) - ); - togglerElements.forEach((el) => (el.style.display = "")); - if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); - }, - - initOnKeyListeners: () => { - // only install a listener if it is really needed - if ( - !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && - !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS - ) - return; - - document.addEventListener("keydown", (event) => { - // bail for input elements - if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; - // bail with special keys - if (event.altKey || event.ctrlKey || event.metaKey) return; - - if (!event.shiftKey) { - switch (event.key) { - case "ArrowLeft": - if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; - - const prevLink = document.querySelector('link[rel="prev"]'); - if (prevLink && prevLink.href) { - window.location.href = prevLink.href; - event.preventDefault(); - } - break; - case "ArrowRight": - if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; - - const nextLink = document.querySelector('link[rel="next"]'); - if (nextLink && nextLink.href) { - window.location.href = nextLink.href; - event.preventDefault(); - } - break; - } - } - - // some keyboard layouts may need Shift to get / - switch (event.key) { - case "/": - if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; - Documentation.focusSearchBar(); - event.preventDefault(); - } - }); - }, -}; - -// quick alias for translations -const _ = Documentation.gettext; - -_ready(Documentation.init); +/* + * doctools.js + * ~~~~~~~~~~~ + * + * Base JavaScript utilities for all Sphinx HTML documentation. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/docs/build/html/_static/documentation_options.js b/docs/build/html/_static/documentation_options.js index 89435bb..b6033cd 100644 --- a/docs/build/html/_static/documentation_options.js +++ b/docs/build/html/_static/documentation_options.js @@ -1,13 +1,13 @@ -const DOCUMENTATION_OPTIONS = { - VERSION: '1.0.0', - LANGUAGE: 'en', - COLLAPSE_INDEX: false, - BUILDER: 'html', - FILE_SUFFIX: '.html', - LINK_SUFFIX: '.html', - HAS_SOURCE: true, - SOURCELINK_SUFFIX: '.txt', - NAVIGATION_WITH_KEYS: false, - SHOW_SEARCH_SUMMARY: true, - ENABLE_SEARCH_SHORTCUTS: true, +const DOCUMENTATION_OPTIONS = { + VERSION: '1.0.0', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, }; \ No newline at end of file diff --git a/docs/build/html/_static/jquery.js b/docs/build/html/_static/jquery.js index c4c6022..49310b5 100644 --- a/docs/build/html/_static/jquery.js +++ b/docs/build/html/_static/jquery.js @@ -1,2 +1,2 @@ -/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=y.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=y.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),y.elements=c+" "+a,j(b)}function f(a){var b=x[a[v]];return b||(b={},w++,a[v]=w,x[w]=b),b}function g(a,c,d){if(c||(c=b),q)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():u.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||t.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),q)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return y.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(y,b.frag)}function j(a){a||(a=b);var d=f(a);return!y.shivCSS||p||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),q||i(a,d),a}function k(a){for(var b,c=a.getElementsByTagName("*"),e=c.length,f=RegExp("^(?:"+d().join("|")+")$","i"),g=[];e--;)b=c[e],f.test(b.nodeName)&&g.push(b.applyElement(l(b)));return g}function l(a){for(var b,c=a.attributes,d=c.length,e=a.ownerDocument.createElement(A+":"+a.nodeName);d--;)b=c[d],b.specified&&e.setAttribute(b.nodeName,b.nodeValue);return e.style.cssText=a.style.cssText,e}function m(a){for(var b,c=a.split("{"),e=c.length,f=RegExp("(^|[\\s,>+~])("+d().join("|")+")(?=[[\\s,>+~#.:]|$)","gi"),g="$1"+A+"\\:$2";e--;)b=c[e]=c[e].split("}"),b[b.length-1]=b[b.length-1].replace(f,g),c[e]=b.join("}");return c.join("{")}function n(a){for(var b=a.length;b--;)a[b].removeNode()}function o(a){function b(){clearTimeout(g._removeSheetTimer),d&&d.removeNode(!0),d=null}var d,e,g=f(a),h=a.namespaces,i=a.parentWindow;return!B||a.printShived?a:("undefined"==typeof h[A]&&h.add(A),i.attachEvent("onbeforeprint",function(){b();for(var f,g,h,i=a.styleSheets,j=[],l=i.length,n=Array(l);l--;)n[l]=i[l];for(;h=n.pop();)if(!h.disabled&&z.test(h.media)){try{f=h.imports,g=f.length}catch(o){g=0}for(l=0;g>l;l++)n.push(f[l]);try{j.push(h.cssText)}catch(o){}}j=m(j.reverse().join("")),e=k(a),d=c(a,j)}),i.attachEvent("onafterprint",function(){n(e),clearTimeout(g._removeSheetTimer),g._removeSheetTimer=setTimeout(b,500)}),a.printShived=!0,a)}var p,q,r="3.7.3",s=a.html5||{},t=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,u=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,v="_html5shiv",w=0,x={};!function(){try{var a=b.createElement("a");a.innerHTML="",p="hidden"in a,q=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){p=!0,q=!0}}();var y={elements:s.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:r,shivCSS:s.shivCSS!==!1,supportsUnknownElements:q,shivMethods:s.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=y,j(b);var z=/^$|\b(?:all|print)\b/,A="html5shiv",B=!q&&function(){var c=b.documentElement;return!("undefined"==typeof b.namespaces||"undefined"==typeof b.parentWindow||"undefined"==typeof c.applyElement||"undefined"==typeof c.removeNode||"undefined"==typeof a.attachEvent)}();y.type+=" print",y.shivPrint=o,o(b),"object"==typeof module&&module.exports&&(module.exports=y)}("undefined"!=typeof window?window:this,document); \ No newline at end of file diff --git a/docs/build/html/_static/js/html5shiv.min.js b/docs/build/html/_static/js/html5shiv.min.js index cd1c674..41830ad 100644 --- a/docs/build/html/_static/js/html5shiv.min.js +++ b/docs/build/html/_static/js/html5shiv.min.js @@ -1,4 +1,4 @@ -/** -* @preserve HTML5 Shiv 3.7.3 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed -*/ +/** +* @preserve HTML5 Shiv 3.7.3 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed +*/ !function(a,b){function c(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=t.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=t.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),t.elements=c+" "+a,j(b)}function f(a){var b=s[a[q]];return b||(b={},r++,a[q]=r,s[r]=b),b}function g(a,c,d){if(c||(c=b),l)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():p.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||o.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),l)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return t.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(t,b.frag)}function j(a){a||(a=b);var d=f(a);return!t.shivCSS||k||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),l||i(a,d),a}var k,l,m="3.7.3-pre",n=a.html5||{},o=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,p=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,q="_html5shiv",r=0,s={};!function(){try{var a=b.createElement("a");a.innerHTML="",k="hidden"in a,l=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){k=!0,l=!0}}();var t={elements:n.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:n.shivCSS!==!1,supportsUnknownElements:l,shivMethods:n.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=t,j(b),"object"==typeof module&&module.exports&&(module.exports=t)}("undefined"!=typeof window?window:this,document); \ No newline at end of file diff --git a/docs/build/html/_static/language_data.js b/docs/build/html/_static/language_data.js index 367b8ed..fd5e0d7 100644 --- a/docs/build/html/_static/language_data.js +++ b/docs/build/html/_static/language_data.js @@ -1,199 +1,199 @@ -/* - * language_data.js - * ~~~~~~~~~~~~~~~~ - * - * This script contains the language-specific data used by searchtools.js, - * namely the list of stopwords, stemmer, scorer and splitter. - * - * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ - -var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; - - -/* Non-minified version is copied as a separate JS file, if available */ - -/** - * Porter Stemmer - */ -var Stemmer = function() { - - var step2list = { - ational: 'ate', - tional: 'tion', - enci: 'ence', - anci: 'ance', - izer: 'ize', - bli: 'ble', - alli: 'al', - entli: 'ent', - eli: 'e', - ousli: 'ous', - ization: 'ize', - ation: 'ate', - ator: 'ate', - alism: 'al', - iveness: 'ive', - fulness: 'ful', - ousness: 'ous', - aliti: 'al', - iviti: 'ive', - biliti: 'ble', - logi: 'log' - }; - - var step3list = { - icate: 'ic', - ative: '', - alize: 'al', - iciti: 'ic', - ical: 'ic', - ful: '', - ness: '' - }; - - var c = "[^aeiou]"; // consonant - var v = "[aeiouy]"; // vowel - var C = c + "[^aeiouy]*"; // consonant sequence - var V = v + "[aeiou]*"; // vowel sequence - - var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 - var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 - var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 - var s_v = "^(" + C + ")?" + v; // vowel in stem - - this.stemWord = function (w) { - var stem; - var suffix; - var firstch; - var origword = w; - - if (w.length < 3) - return w; - - var re; - var re2; - var re3; - var re4; - - firstch = w.substr(0,1); - if (firstch == "y") - w = firstch.toUpperCase() + w.substr(1); - - // Step 1a - re = /^(.+?)(ss|i)es$/; - re2 = /^(.+?)([^s])s$/; - - if (re.test(w)) - w = w.replace(re,"$1$2"); - else if (re2.test(w)) - w = w.replace(re2,"$1$2"); - - // Step 1b - re = /^(.+?)eed$/; - re2 = /^(.+?)(ed|ing)$/; - if (re.test(w)) { - var fp = re.exec(w); - re = new RegExp(mgr0); - if (re.test(fp[1])) { - re = /.$/; - w = w.replace(re,""); - } - } - else if (re2.test(w)) { - var fp = re2.exec(w); - stem = fp[1]; - re2 = new RegExp(s_v); - if (re2.test(stem)) { - w = stem; - re2 = /(at|bl|iz)$/; - re3 = new RegExp("([^aeiouylsz])\\1$"); - re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); - if (re2.test(w)) - w = w + "e"; - else if (re3.test(w)) { - re = /.$/; - w = w.replace(re,""); - } - else if (re4.test(w)) - w = w + "e"; - } - } - - // Step 1c - re = /^(.+?)y$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - re = new RegExp(s_v); - if (re.test(stem)) - w = stem + "i"; - } - - // Step 2 - re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - suffix = fp[2]; - re = new RegExp(mgr0); - if (re.test(stem)) - w = stem + step2list[suffix]; - } - - // Step 3 - re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - suffix = fp[2]; - re = new RegExp(mgr0); - if (re.test(stem)) - w = stem + step3list[suffix]; - } - - // Step 4 - re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; - re2 = /^(.+?)(s|t)(ion)$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - re = new RegExp(mgr1); - if (re.test(stem)) - w = stem; - } - else if (re2.test(w)) { - var fp = re2.exec(w); - stem = fp[1] + fp[2]; - re2 = new RegExp(mgr1); - if (re2.test(stem)) - w = stem; - } - - // Step 5 - re = /^(.+?)e$/; - if (re.test(w)) { - var fp = re.exec(w); - stem = fp[1]; - re = new RegExp(mgr1); - re2 = new RegExp(meq1); - re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); - if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) - w = stem; - } - re = /ll$/; - re2 = new RegExp(mgr1); - if (re.test(w) && re2.test(w)) { - re = /.$/; - w = w.replace(re,""); - } - - // and turn initial Y back to y - if (firstch == "y") - w = firstch.toLowerCase() + w.substr(1); - return w; - } -} - +/* + * language_data.js + * ~~~~~~~~~~~~~~~~ + * + * This script contains the language-specific data used by searchtools.js, + * namely the list of stopwords, stemmer, scorer and splitter. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; + + +/* Non-minified version is copied as a separate JS file, if available */ + +/** + * Porter Stemmer + */ +var Stemmer = function() { + + var step2list = { + ational: 'ate', + tional: 'tion', + enci: 'ence', + anci: 'ance', + izer: 'ize', + bli: 'ble', + alli: 'al', + entli: 'ent', + eli: 'e', + ousli: 'ous', + ization: 'ize', + ation: 'ate', + ator: 'ate', + alism: 'al', + iveness: 'ive', + fulness: 'ful', + ousness: 'ous', + aliti: 'al', + iviti: 'ive', + biliti: 'ble', + logi: 'log' + }; + + var step3list = { + icate: 'ic', + ative: '', + alize: 'al', + iciti: 'ic', + ical: 'ic', + ful: '', + ness: '' + }; + + var c = "[^aeiou]"; // consonant + var v = "[aeiouy]"; // vowel + var C = c + "[^aeiouy]*"; // consonant sequence + var V = v + "[aeiou]*"; // vowel sequence + + var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + diff --git a/docs/build/html/_static/nbsphinx-broken-thumbnail.svg b/docs/build/html/_static/nbsphinx-broken-thumbnail.svg index 4919ca8..5482ebd 100644 --- a/docs/build/html/_static/nbsphinx-broken-thumbnail.svg +++ b/docs/build/html/_static/nbsphinx-broken-thumbnail.svg @@ -1,9 +1,9 @@ - - - - + + + + diff --git a/docs/build/html/_static/nbsphinx-code-cells.css b/docs/build/html/_static/nbsphinx-code-cells.css index a3fb27c..f562950 100644 --- a/docs/build/html/_static/nbsphinx-code-cells.css +++ b/docs/build/html/_static/nbsphinx-code-cells.css @@ -1,259 +1,259 @@ -/* remove conflicting styling from Sphinx themes */ -div.nbinput.container div.prompt *, -div.nboutput.container div.prompt *, -div.nbinput.container div.input_area pre, -div.nboutput.container div.output_area pre, -div.nbinput.container div.input_area .highlight, -div.nboutput.container div.output_area .highlight { - border: none; - padding: 0; - margin: 0; - box-shadow: none; -} - -div.nbinput.container > div[class*=highlight], -div.nboutput.container > div[class*=highlight] { - margin: 0; -} - -div.nbinput.container div.prompt *, -div.nboutput.container div.prompt * { - background: none; -} - -div.nboutput.container div.output_area .highlight, -div.nboutput.container div.output_area pre { - background: unset; -} - -div.nboutput.container div.output_area div.highlight { - color: unset; /* override Pygments text color */ -} - -/* avoid gaps between output lines */ -div.nboutput.container div[class*=highlight] pre { - line-height: normal; -} - -/* input/output containers */ -div.nbinput.container, -div.nboutput.container { - display: -webkit-flex; - display: flex; - align-items: flex-start; - margin: 0; - width: 100%; -} -@media (max-width: 540px) { - div.nbinput.container, - div.nboutput.container { - flex-direction: column; - } -} - -/* input container */ -div.nbinput.container { - padding-top: 5px; -} - -/* last container */ -div.nblast.container { - padding-bottom: 5px; -} - -/* input prompt */ -div.nbinput.container div.prompt pre, -/* for sphinx_immaterial theme: */ -div.nbinput.container div.prompt pre > code { - color: #307FC1; -} - -/* output prompt */ -div.nboutput.container div.prompt pre, -/* for sphinx_immaterial theme: */ -div.nboutput.container div.prompt pre > code { - color: #BF5B3D; -} - -/* all prompts */ -div.nbinput.container div.prompt, -div.nboutput.container div.prompt { - width: 4.5ex; - padding-top: 5px; - position: relative; - user-select: none; -} - -div.nbinput.container div.prompt > div, -div.nboutput.container div.prompt > div { - position: absolute; - right: 0; - margin-right: 0.3ex; -} - -@media (max-width: 540px) { - div.nbinput.container div.prompt, - div.nboutput.container div.prompt { - width: unset; - text-align: left; - padding: 0.4em; - } - div.nboutput.container div.prompt.empty { - padding: 0; - } - - div.nbinput.container div.prompt > div, - div.nboutput.container div.prompt > div { - position: unset; - } -} - -/* disable scrollbars and line breaks on prompts */ -div.nbinput.container div.prompt pre, -div.nboutput.container div.prompt pre { - overflow: hidden; - white-space: pre; -} - -/* input/output area */ -div.nbinput.container div.input_area, -div.nboutput.container div.output_area { - -webkit-flex: 1; - flex: 1; - overflow: auto; -} -@media (max-width: 540px) { - div.nbinput.container div.input_area, - div.nboutput.container div.output_area { - width: 100%; - } -} - -/* input area */ -div.nbinput.container div.input_area { - border: 1px solid #e0e0e0; - border-radius: 2px; - /*background: #f5f5f5;*/ -} - -/* override MathJax center alignment in output cells */ -div.nboutput.container div[class*=MathJax] { - text-align: left !important; -} - -/* override sphinx.ext.imgmath center alignment in output cells */ -div.nboutput.container div.math p { - text-align: left; -} - -/* standard error */ -div.nboutput.container div.output_area.stderr { - background: #fdd; -} - -/* ANSI colors */ -.ansi-black-fg { color: #3E424D; } -.ansi-black-bg { background-color: #3E424D; } -.ansi-black-intense-fg { color: #282C36; } -.ansi-black-intense-bg { background-color: #282C36; } -.ansi-red-fg { color: #E75C58; } -.ansi-red-bg { background-color: #E75C58; } -.ansi-red-intense-fg { color: #B22B31; } -.ansi-red-intense-bg { background-color: #B22B31; } -.ansi-green-fg { color: #00A250; } -.ansi-green-bg { background-color: #00A250; } -.ansi-green-intense-fg { color: #007427; } -.ansi-green-intense-bg { background-color: #007427; } -.ansi-yellow-fg { color: #DDB62B; } -.ansi-yellow-bg { background-color: #DDB62B; } -.ansi-yellow-intense-fg { color: #B27D12; } -.ansi-yellow-intense-bg { background-color: #B27D12; } -.ansi-blue-fg { color: #208FFB; } -.ansi-blue-bg { background-color: #208FFB; } -.ansi-blue-intense-fg { color: #0065CA; } -.ansi-blue-intense-bg { background-color: #0065CA; } -.ansi-magenta-fg { color: #D160C4; } -.ansi-magenta-bg { background-color: #D160C4; } -.ansi-magenta-intense-fg { color: #A03196; } -.ansi-magenta-intense-bg { background-color: #A03196; } -.ansi-cyan-fg { color: #60C6C8; } -.ansi-cyan-bg { background-color: #60C6C8; } -.ansi-cyan-intense-fg { color: #258F8F; } -.ansi-cyan-intense-bg { background-color: #258F8F; } -.ansi-white-fg { color: #C5C1B4; } -.ansi-white-bg { background-color: #C5C1B4; } -.ansi-white-intense-fg { color: #A1A6B2; } -.ansi-white-intense-bg { background-color: #A1A6B2; } - -.ansi-default-inverse-fg { color: #FFFFFF; } -.ansi-default-inverse-bg { background-color: #000000; } - -.ansi-bold { font-weight: bold; } -.ansi-underline { text-decoration: underline; } - - -div.nbinput.container div.input_area div[class*=highlight] > pre, -div.nboutput.container div.output_area div[class*=highlight] > pre, -div.nboutput.container div.output_area div[class*=highlight].math, -div.nboutput.container div.output_area.rendered_html, -div.nboutput.container div.output_area > div.output_javascript, -div.nboutput.container div.output_area:not(.rendered_html) > img{ - padding: 5px; - margin: 0; -} - -/* fix copybtn overflow problem in chromium (needed for 'sphinx_copybutton') */ -div.nbinput.container div.input_area > div[class^='highlight'], -div.nboutput.container div.output_area > div[class^='highlight']{ - overflow-y: hidden; -} - -/* hide copy button on prompts for 'sphinx_copybutton' extension ... */ -.prompt .copybtn, -/* ... and 'sphinx_immaterial' theme */ -.prompt .md-clipboard.md-icon { - display: none; -} - -/* Some additional styling taken form the Jupyter notebook CSS */ -.jp-RenderedHTMLCommon table, -div.rendered_html table { - border: none; - border-collapse: collapse; - border-spacing: 0; - color: black; - font-size: 12px; - table-layout: fixed; -} -.jp-RenderedHTMLCommon thead, -div.rendered_html thead { - border-bottom: 1px solid black; - vertical-align: bottom; -} -.jp-RenderedHTMLCommon tr, -.jp-RenderedHTMLCommon th, -.jp-RenderedHTMLCommon td, -div.rendered_html tr, -div.rendered_html th, -div.rendered_html td { - text-align: right; - vertical-align: middle; - padding: 0.5em 0.5em; - line-height: normal; - white-space: normal; - max-width: none; - border: none; -} -.jp-RenderedHTMLCommon th, -div.rendered_html th { - font-weight: bold; -} -.jp-RenderedHTMLCommon tbody tr:nth-child(odd), -div.rendered_html tbody tr:nth-child(odd) { - background: #f5f5f5; -} -.jp-RenderedHTMLCommon tbody tr:hover, -div.rendered_html tbody tr:hover { - background: rgba(66, 165, 245, 0.2); -} - +/* remove conflicting styling from Sphinx themes */ +div.nbinput.container div.prompt *, +div.nboutput.container div.prompt *, +div.nbinput.container div.input_area pre, +div.nboutput.container div.output_area pre, +div.nbinput.container div.input_area .highlight, +div.nboutput.container div.output_area .highlight { + border: none; + padding: 0; + margin: 0; + box-shadow: none; +} + +div.nbinput.container > div[class*=highlight], +div.nboutput.container > div[class*=highlight] { + margin: 0; +} + +div.nbinput.container div.prompt *, +div.nboutput.container div.prompt * { + background: none; +} + +div.nboutput.container div.output_area .highlight, +div.nboutput.container div.output_area pre { + background: unset; +} + +div.nboutput.container div.output_area div.highlight { + color: unset; /* override Pygments text color */ +} + +/* avoid gaps between output lines */ +div.nboutput.container div[class*=highlight] pre { + line-height: normal; +} + +/* input/output containers */ +div.nbinput.container, +div.nboutput.container { + display: -webkit-flex; + display: flex; + align-items: flex-start; + margin: 0; + width: 100%; +} +@media (max-width: 540px) { + div.nbinput.container, + div.nboutput.container { + flex-direction: column; + } +} + +/* input container */ +div.nbinput.container { + padding-top: 5px; +} + +/* last container */ +div.nblast.container { + padding-bottom: 5px; +} + +/* input prompt */ +div.nbinput.container div.prompt pre, +/* for sphinx_immaterial theme: */ +div.nbinput.container div.prompt pre > code { + color: #307FC1; +} + +/* output prompt */ +div.nboutput.container div.prompt pre, +/* for sphinx_immaterial theme: */ +div.nboutput.container div.prompt pre > code { + color: #BF5B3D; +} + +/* all prompts */ +div.nbinput.container div.prompt, +div.nboutput.container div.prompt { + width: 4.5ex; + padding-top: 5px; + position: relative; + user-select: none; +} + +div.nbinput.container div.prompt > div, +div.nboutput.container div.prompt > div { + position: absolute; + right: 0; + margin-right: 0.3ex; +} + +@media (max-width: 540px) { + div.nbinput.container div.prompt, + div.nboutput.container div.prompt { + width: unset; + text-align: left; + padding: 0.4em; + } + div.nboutput.container div.prompt.empty { + padding: 0; + } + + div.nbinput.container div.prompt > div, + div.nboutput.container div.prompt > div { + position: unset; + } +} + +/* disable scrollbars and line breaks on prompts */ +div.nbinput.container div.prompt pre, +div.nboutput.container div.prompt pre { + overflow: hidden; + white-space: pre; +} + +/* input/output area */ +div.nbinput.container div.input_area, +div.nboutput.container div.output_area { + -webkit-flex: 1; + flex: 1; + overflow: auto; +} +@media (max-width: 540px) { + div.nbinput.container div.input_area, + div.nboutput.container div.output_area { + width: 100%; + } +} + +/* input area */ +div.nbinput.container div.input_area { + border: 1px solid #e0e0e0; + border-radius: 2px; + /*background: #f5f5f5;*/ +} + +/* override MathJax center alignment in output cells */ +div.nboutput.container div[class*=MathJax] { + text-align: left !important; +} + +/* override sphinx.ext.imgmath center alignment in output cells */ +div.nboutput.container div.math p { + text-align: left; +} + +/* standard error */ +div.nboutput.container div.output_area.stderr { + background: #fdd; +} + +/* ANSI colors */ +.ansi-black-fg { color: #3E424D; } +.ansi-black-bg { background-color: #3E424D; } +.ansi-black-intense-fg { color: #282C36; } +.ansi-black-intense-bg { background-color: #282C36; } +.ansi-red-fg { color: #E75C58; } +.ansi-red-bg { background-color: #E75C58; } +.ansi-red-intense-fg { color: #B22B31; } +.ansi-red-intense-bg { background-color: #B22B31; } +.ansi-green-fg { color: #00A250; } +.ansi-green-bg { background-color: #00A250; } +.ansi-green-intense-fg { color: #007427; } +.ansi-green-intense-bg { background-color: #007427; } +.ansi-yellow-fg { color: #DDB62B; } +.ansi-yellow-bg { background-color: #DDB62B; } +.ansi-yellow-intense-fg { color: #B27D12; } +.ansi-yellow-intense-bg { background-color: #B27D12; } +.ansi-blue-fg { color: #208FFB; } +.ansi-blue-bg { background-color: #208FFB; } +.ansi-blue-intense-fg { color: #0065CA; } +.ansi-blue-intense-bg { background-color: #0065CA; } +.ansi-magenta-fg { color: #D160C4; } +.ansi-magenta-bg { background-color: #D160C4; } +.ansi-magenta-intense-fg { color: #A03196; } +.ansi-magenta-intense-bg { background-color: #A03196; } +.ansi-cyan-fg { color: #60C6C8; } +.ansi-cyan-bg { background-color: #60C6C8; } +.ansi-cyan-intense-fg { color: #258F8F; } +.ansi-cyan-intense-bg { background-color: #258F8F; } +.ansi-white-fg { color: #C5C1B4; } +.ansi-white-bg { background-color: #C5C1B4; } +.ansi-white-intense-fg { color: #A1A6B2; } +.ansi-white-intense-bg { background-color: #A1A6B2; } + +.ansi-default-inverse-fg { color: #FFFFFF; } +.ansi-default-inverse-bg { background-color: #000000; } + +.ansi-bold { font-weight: bold; } +.ansi-underline { text-decoration: underline; } + + +div.nbinput.container div.input_area div[class*=highlight] > pre, +div.nboutput.container div.output_area div[class*=highlight] > pre, +div.nboutput.container div.output_area div[class*=highlight].math, +div.nboutput.container div.output_area.rendered_html, +div.nboutput.container div.output_area > div.output_javascript, +div.nboutput.container div.output_area:not(.rendered_html) > img{ + padding: 5px; + margin: 0; +} + +/* fix copybtn overflow problem in chromium (needed for 'sphinx_copybutton') */ +div.nbinput.container div.input_area > div[class^='highlight'], +div.nboutput.container div.output_area > div[class^='highlight']{ + overflow-y: hidden; +} + +/* hide copy button on prompts for 'sphinx_copybutton' extension ... */ +.prompt .copybtn, +/* ... and 'sphinx_immaterial' theme */ +.prompt .md-clipboard.md-icon { + display: none; +} + +/* Some additional styling taken form the Jupyter notebook CSS */ +.jp-RenderedHTMLCommon table, +div.rendered_html table { + border: none; + border-collapse: collapse; + border-spacing: 0; + color: black; + font-size: 12px; + table-layout: fixed; +} +.jp-RenderedHTMLCommon thead, +div.rendered_html thead { + border-bottom: 1px solid black; + vertical-align: bottom; +} +.jp-RenderedHTMLCommon tr, +.jp-RenderedHTMLCommon th, +.jp-RenderedHTMLCommon td, +div.rendered_html tr, +div.rendered_html th, +div.rendered_html td { + text-align: right; + vertical-align: middle; + padding: 0.5em 0.5em; + line-height: normal; + white-space: normal; + max-width: none; + border: none; +} +.jp-RenderedHTMLCommon th, +div.rendered_html th { + font-weight: bold; +} +.jp-RenderedHTMLCommon tbody tr:nth-child(odd), +div.rendered_html tbody tr:nth-child(odd) { + background: #f5f5f5; +} +.jp-RenderedHTMLCommon tbody tr:hover, +div.rendered_html tbody tr:hover { + background: rgba(66, 165, 245, 0.2); +} + diff --git a/docs/build/html/_static/nbsphinx-gallery.css b/docs/build/html/_static/nbsphinx-gallery.css index 365c27a..765e5fb 100644 --- a/docs/build/html/_static/nbsphinx-gallery.css +++ b/docs/build/html/_static/nbsphinx-gallery.css @@ -1,31 +1,31 @@ -.nbsphinx-gallery { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); - gap: 5px; - margin-top: 1em; - margin-bottom: 1em; -} - -.nbsphinx-gallery > a { - padding: 5px; - border: 1px dotted currentColor; - border-radius: 2px; - text-align: center; -} - -.nbsphinx-gallery > a:hover { - border-style: solid; -} - -.nbsphinx-gallery img { - max-width: 100%; - max-height: 100%; -} - -.nbsphinx-gallery > a > div:first-child { - display: flex; - align-items: start; - justify-content: center; - height: 120px; - margin-bottom: 5px; -} +.nbsphinx-gallery { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 5px; + margin-top: 1em; + margin-bottom: 1em; +} + +.nbsphinx-gallery > a { + padding: 5px; + border: 1px dotted currentColor; + border-radius: 2px; + text-align: center; +} + +.nbsphinx-gallery > a:hover { + border-style: solid; +} + +.nbsphinx-gallery img { + max-width: 100%; + max-height: 100%; +} + +.nbsphinx-gallery > a > div:first-child { + display: flex; + align-items: start; + justify-content: center; + height: 120px; + margin-bottom: 5px; +} diff --git a/docs/build/html/_static/nbsphinx-no-thumbnail.svg b/docs/build/html/_static/nbsphinx-no-thumbnail.svg index 9dca758..15f7632 100644 --- a/docs/build/html/_static/nbsphinx-no-thumbnail.svg +++ b/docs/build/html/_static/nbsphinx-no-thumbnail.svg @@ -1,9 +1,9 @@ - - - - + + + + diff --git a/docs/build/html/_static/pygments.css b/docs/build/html/_static/pygments.css index 84ab303..26c5c65 100644 --- a/docs/build/html/_static/pygments.css +++ b/docs/build/html/_static/pygments.css @@ -1,75 +1,75 @@ -pre { line-height: 125%; } -td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } -span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } -td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } -span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } -.highlight .hll { background-color: #ffffcc } -.highlight { background: #f8f8f8; } -.highlight .c { color: #3D7B7B; font-style: italic } /* Comment */ -.highlight .err { border: 1px solid #FF0000 } /* Error */ -.highlight .k { color: #008000; font-weight: bold } /* Keyword */ -.highlight .o { color: #666666 } /* Operator */ -.highlight .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */ -.highlight .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */ -.highlight .cp { color: #9C6500 } /* Comment.Preproc */ -.highlight .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */ -.highlight .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */ -.highlight .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */ -.highlight .gd { color: #A00000 } /* Generic.Deleted */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */ -.highlight .gr { color: #E40000 } /* Generic.Error */ -.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ -.highlight .gi { color: #008400 } /* Generic.Inserted */ -.highlight .go { color: #717171 } /* Generic.Output */ -.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -.highlight .gt { color: #0044DD } /* Generic.Traceback */ -.highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ -.highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ -.highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ -.highlight .kp { color: #008000 } /* Keyword.Pseudo */ -.highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ -.highlight .kt { color: #B00040 } /* Keyword.Type */ -.highlight .m { color: #666666 } /* Literal.Number */ -.highlight .s { color: #BA2121 } /* Literal.String */ -.highlight .na { color: #687822 } /* Name.Attribute */ -.highlight .nb { color: #008000 } /* Name.Builtin */ -.highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */ -.highlight .no { color: #880000 } /* Name.Constant */ -.highlight .nd { color: #AA22FF } /* Name.Decorator */ -.highlight .ni { color: #717171; font-weight: bold } /* Name.Entity */ -.highlight .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */ -.highlight .nf { color: #0000FF } /* Name.Function */ -.highlight .nl { color: #767600 } /* Name.Label */ -.highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ -.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ -.highlight .nv { color: #19177C } /* Name.Variable */ -.highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ -.highlight .w { color: #bbbbbb } /* Text.Whitespace */ -.highlight .mb { color: #666666 } /* Literal.Number.Bin */ -.highlight .mf { color: #666666 } /* Literal.Number.Float */ -.highlight .mh { color: #666666 } /* Literal.Number.Hex */ -.highlight .mi { color: #666666 } /* Literal.Number.Integer */ -.highlight .mo { color: #666666 } /* Literal.Number.Oct */ -.highlight .sa { color: #BA2121 } /* Literal.String.Affix */ -.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ -.highlight .sc { color: #BA2121 } /* Literal.String.Char */ -.highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */ -.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ -.highlight .s2 { color: #BA2121 } /* Literal.String.Double */ -.highlight .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */ -.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ -.highlight .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */ -.highlight .sx { color: #008000 } /* Literal.String.Other */ -.highlight .sr { color: #A45A77 } /* Literal.String.Regex */ -.highlight .s1 { color: #BA2121 } /* Literal.String.Single */ -.highlight .ss { color: #19177C } /* Literal.String.Symbol */ -.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ -.highlight .fm { color: #0000FF } /* Name.Function.Magic */ -.highlight .vc { color: #19177C } /* Name.Variable.Class */ -.highlight .vg { color: #19177C } /* Name.Variable.Global */ -.highlight .vi { color: #19177C } /* Name.Variable.Instance */ -.highlight .vm { color: #19177C } /* Name.Variable.Magic */ +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight .hll { background-color: #ffffcc } +.highlight { background: #f8f8f8; } +.highlight .c { color: #3D7B7B; font-style: italic } /* Comment */ +.highlight .err { border: 1px solid #FF0000 } /* Error */ +.highlight .k { color: #008000; font-weight: bold } /* Keyword */ +.highlight .o { color: #666666 } /* Operator */ +.highlight .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */ +.highlight .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #9C6500 } /* Comment.Preproc */ +.highlight .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */ +.highlight .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */ +.highlight .gd { color: #A00000 } /* Generic.Deleted */ +.highlight .ge { font-style: italic } /* Generic.Emph */ +.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */ +.highlight .gr { color: #E40000 } /* Generic.Error */ +.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #008400 } /* Generic.Inserted */ +.highlight .go { color: #717171 } /* Generic.Output */ +.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +.highlight .gs { font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #0044DD } /* Generic.Traceback */ +.highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { color: #008000 } /* Keyword.Pseudo */ +.highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #B00040 } /* Keyword.Type */ +.highlight .m { color: #666666 } /* Literal.Number */ +.highlight .s { color: #BA2121 } /* Literal.String */ +.highlight .na { color: #687822 } /* Name.Attribute */ +.highlight .nb { color: #008000 } /* Name.Builtin */ +.highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */ +.highlight .no { color: #880000 } /* Name.Constant */ +.highlight .nd { color: #AA22FF } /* Name.Decorator */ +.highlight .ni { color: #717171; font-weight: bold } /* Name.Entity */ +.highlight .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */ +.highlight .nf { color: #0000FF } /* Name.Function */ +.highlight .nl { color: #767600 } /* Name.Label */ +.highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ +.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #19177C } /* Name.Variable */ +.highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ +.highlight .w { color: #bbbbbb } /* Text.Whitespace */ +.highlight .mb { color: #666666 } /* Literal.Number.Bin */ +.highlight .mf { color: #666666 } /* Literal.Number.Float */ +.highlight .mh { color: #666666 } /* Literal.Number.Hex */ +.highlight .mi { color: #666666 } /* Literal.Number.Integer */ +.highlight .mo { color: #666666 } /* Literal.Number.Oct */ +.highlight .sa { color: #BA2121 } /* Literal.String.Affix */ +.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ +.highlight .sc { color: #BA2121 } /* Literal.String.Char */ +.highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */ +.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #BA2121 } /* Literal.String.Double */ +.highlight .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */ +.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ +.highlight .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */ +.highlight .sx { color: #008000 } /* Literal.String.Other */ +.highlight .sr { color: #A45A77 } /* Literal.String.Regex */ +.highlight .s1 { color: #BA2121 } /* Literal.String.Single */ +.highlight .ss { color: #19177C } /* Literal.String.Symbol */ +.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #0000FF } /* Name.Function.Magic */ +.highlight .vc { color: #19177C } /* Name.Variable.Class */ +.highlight .vg { color: #19177C } /* Name.Variable.Global */ +.highlight .vi { color: #19177C } /* Name.Variable.Instance */ +.highlight .vm { color: #19177C } /* Name.Variable.Magic */ .highlight .il { color: #666666 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/build/html/_static/searchtools.js b/docs/build/html/_static/searchtools.js index 92da3f8..390237c 100644 --- a/docs/build/html/_static/searchtools.js +++ b/docs/build/html/_static/searchtools.js @@ -1,619 +1,619 @@ -/* - * searchtools.js - * ~~~~~~~~~~~~~~~~ - * - * Sphinx JavaScript utilities for the full-text search. - * - * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. - * :license: BSD, see LICENSE for details. - * - */ -"use strict"; - -/** - * Simple result scoring code. - */ -if (typeof Scorer === "undefined") { - var Scorer = { - // Implement the following function to further tweak the score for each result - // The function takes a result array [docname, title, anchor, descr, score, filename] - // and returns the new score. - /* - score: result => { - const [docname, title, anchor, descr, score, filename] = result - return score - }, - */ - - // query matches the full name of an object - objNameMatch: 11, - // or matches in the last dotted part of the object name - objPartialMatch: 6, - // Additive scores depending on the priority of the object - objPrio: { - 0: 15, // used to be importantResults - 1: 5, // used to be objectResults - 2: -5, // used to be unimportantResults - }, - // Used when the priority is not in the mapping. - objPrioDefault: 0, - - // query found in title - title: 15, - partialTitle: 7, - // query found in terms - term: 5, - partialTerm: 2, - }; -} - -const _removeChildren = (element) => { - while (element && element.lastChild) element.removeChild(element.lastChild); -}; - -/** - * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping - */ -const _escapeRegExp = (string) => - string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string - -const _displayItem = (item, searchTerms, highlightTerms) => { - const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; - const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; - const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX; - const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; - const contentRoot = document.documentElement.dataset.content_root; - - const [docName, title, anchor, descr, score, _filename] = item; - - let listItem = document.createElement("li"); - let requestUrl; - let linkUrl; - if (docBuilder === "dirhtml") { - // dirhtml builder - let dirname = docName + "/"; - if (dirname.match(/\/index\/$/)) - dirname = dirname.substring(0, dirname.length - 6); - else if (dirname === "index/") dirname = ""; - requestUrl = contentRoot + dirname; - linkUrl = requestUrl; - } else { - // normal html builders - requestUrl = contentRoot + docName + docFileSuffix; - linkUrl = docName + docLinkSuffix; - } - let linkEl = listItem.appendChild(document.createElement("a")); - linkEl.href = linkUrl + anchor; - linkEl.dataset.score = score; - linkEl.innerHTML = title; - if (descr) { - listItem.appendChild(document.createElement("span")).innerHTML = - " (" + descr + ")"; - // highlight search terms in the description - if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js - highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); - } - else if (showSearchSummary) - fetch(requestUrl) - .then((responseData) => responseData.text()) - .then((data) => { - if (data) - listItem.appendChild( - Search.makeSearchSummary(data, searchTerms, anchor) - ); - // highlight search terms in the summary - if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js - highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); - }); - Search.output.appendChild(listItem); -}; -const _finishSearch = (resultCount) => { - Search.stopPulse(); - Search.title.innerText = _("Search Results"); - if (!resultCount) - Search.status.innerText = Documentation.gettext( - "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." - ); - else - Search.status.innerText = _( - "Search finished, found ${resultCount} page(s) matching the search query." - ).replace('${resultCount}', resultCount); -}; -const _displayNextItem = ( - results, - resultCount, - searchTerms, - highlightTerms, -) => { - // results left, load the summary and display it - // this is intended to be dynamic (don't sub resultsCount) - if (results.length) { - _displayItem(results.pop(), searchTerms, highlightTerms); - setTimeout( - () => _displayNextItem(results, resultCount, searchTerms, highlightTerms), - 5 - ); - } - // search finished, update title and status message - else _finishSearch(resultCount); -}; -// Helper function used by query() to order search results. -// Each input is an array of [docname, title, anchor, descr, score, filename]. -// Order the results by score (in opposite order of appearance, since the -// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically. -const _orderResultsByScoreThenName = (a, b) => { - const leftScore = a[4]; - const rightScore = b[4]; - if (leftScore === rightScore) { - // same score: sort alphabetically - const leftTitle = a[1].toLowerCase(); - const rightTitle = b[1].toLowerCase(); - if (leftTitle === rightTitle) return 0; - return leftTitle > rightTitle ? -1 : 1; // inverted is intentional - } - return leftScore > rightScore ? 1 : -1; -}; - -/** - * Default splitQuery function. Can be overridden in ``sphinx.search`` with a - * custom function per language. - * - * The regular expression works by splitting the string on consecutive characters - * that are not Unicode letters, numbers, underscores, or emoji characters. - * This is the same as ``\W+`` in Python, preserving the surrogate pair area. - */ -if (typeof splitQuery === "undefined") { - var splitQuery = (query) => query - .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) - .filter(term => term) // remove remaining empty strings -} - -/** - * Search Module - */ -const Search = { - _index: null, - _queued_query: null, - _pulse_status: -1, - - htmlToText: (htmlString, anchor) => { - const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); - for (const removalQuery of [".headerlinks", "script", "style"]) { - htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() }); - } - if (anchor) { - const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`); - if (anchorContent) return anchorContent.textContent; - - console.warn( - `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.` - ); - } - - // if anchor not specified or not found, fall back to main content - const docContent = htmlElement.querySelector('[role="main"]'); - if (docContent) return docContent.textContent; - - console.warn( - "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template." - ); - return ""; - }, - - init: () => { - const query = new URLSearchParams(window.location.search).get("q"); - document - .querySelectorAll('input[name="q"]') - .forEach((el) => (el.value = query)); - if (query) Search.performSearch(query); - }, - - loadIndex: (url) => - (document.body.appendChild(document.createElement("script")).src = url), - - setIndex: (index) => { - Search._index = index; - if (Search._queued_query !== null) { - const query = Search._queued_query; - Search._queued_query = null; - Search.query(query); - } - }, - - hasIndex: () => Search._index !== null, - - deferQuery: (query) => (Search._queued_query = query), - - stopPulse: () => (Search._pulse_status = -1), - - startPulse: () => { - if (Search._pulse_status >= 0) return; - - const pulse = () => { - Search._pulse_status = (Search._pulse_status + 1) % 4; - Search.dots.innerText = ".".repeat(Search._pulse_status); - if (Search._pulse_status >= 0) window.setTimeout(pulse, 500); - }; - pulse(); - }, - - /** - * perform a search for something (or wait until index is loaded) - */ - performSearch: (query) => { - // create the required interface elements - const searchText = document.createElement("h2"); - searchText.textContent = _("Searching"); - const searchSummary = document.createElement("p"); - searchSummary.classList.add("search-summary"); - searchSummary.innerText = ""; - const searchList = document.createElement("ul"); - searchList.classList.add("search"); - - const out = document.getElementById("search-results"); - Search.title = out.appendChild(searchText); - Search.dots = Search.title.appendChild(document.createElement("span")); - Search.status = out.appendChild(searchSummary); - Search.output = out.appendChild(searchList); - - const searchProgress = document.getElementById("search-progress"); - // Some themes don't use the search progress node - if (searchProgress) { - searchProgress.innerText = _("Preparing search..."); - } - Search.startPulse(); - - // index already loaded, the browser was quick! - if (Search.hasIndex()) Search.query(query); - else Search.deferQuery(query); - }, - - _parseQuery: (query) => { - // stem the search terms and add them to the correct list - const stemmer = new Stemmer(); - const searchTerms = new Set(); - const excludedTerms = new Set(); - const highlightTerms = new Set(); - const objectTerms = new Set(splitQuery(query.toLowerCase().trim())); - splitQuery(query.trim()).forEach((queryTerm) => { - const queryTermLower = queryTerm.toLowerCase(); - - // maybe skip this "word" - // stopwords array is from language_data.js - if ( - stopwords.indexOf(queryTermLower) !== -1 || - queryTerm.match(/^\d+$/) - ) - return; - - // stem the word - let word = stemmer.stemWord(queryTermLower); - // select the correct list - if (word[0] === "-") excludedTerms.add(word.substr(1)); - else { - searchTerms.add(word); - highlightTerms.add(queryTermLower); - } - }); - - if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js - localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" ")) - } - - // console.debug("SEARCH: searching for:"); - // console.info("required: ", [...searchTerms]); - // console.info("excluded: ", [...excludedTerms]); - - return [query, searchTerms, excludedTerms, highlightTerms, objectTerms]; - }, - - /** - * execute search (requires search index to be loaded) - */ - _performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => { - const filenames = Search._index.filenames; - const docNames = Search._index.docnames; - const titles = Search._index.titles; - const allTitles = Search._index.alltitles; - const indexEntries = Search._index.indexentries; - - // Collect multiple result groups to be sorted separately and then ordered. - // Each is an array of [docname, title, anchor, descr, score, filename]. - const normalResults = []; - const nonMainIndexResults = []; - - _removeChildren(document.getElementById("search-progress")); - - const queryLower = query.toLowerCase().trim(); - for (const [title, foundTitles] of Object.entries(allTitles)) { - if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { - for (const [file, id] of foundTitles) { - let score = Math.round(100 * queryLower.length / title.length) - normalResults.push([ - docNames[file], - titles[file] !== title ? `${titles[file]} > ${title}` : title, - id !== null ? "#" + id : "", - null, - score, - filenames[file], - ]); - } - } - } - - // search for explicit entries in index directives - for (const [entry, foundEntries] of Object.entries(indexEntries)) { - if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) { - for (const [file, id, isMain] of foundEntries) { - const score = Math.round(100 * queryLower.length / entry.length); - const result = [ - docNames[file], - titles[file], - id ? "#" + id : "", - null, - score, - filenames[file], - ]; - if (isMain) { - normalResults.push(result); - } else { - nonMainIndexResults.push(result); - } - } - } - } - - // lookup as object - objectTerms.forEach((term) => - normalResults.push(...Search.performObjectSearch(term, objectTerms)) - ); - - // lookup as search terms in fulltext - normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms)); - - // let the scorer override scores with a custom scoring function - if (Scorer.score) { - normalResults.forEach((item) => (item[4] = Scorer.score(item))); - nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item))); - } - - // Sort each group of results by score and then alphabetically by name. - normalResults.sort(_orderResultsByScoreThenName); - nonMainIndexResults.sort(_orderResultsByScoreThenName); - - // Combine the result groups in (reverse) order. - // Non-main index entries are typically arbitrary cross-references, - // so display them after other results. - let results = [...nonMainIndexResults, ...normalResults]; - - // remove duplicate search results - // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept - let seen = new Set(); - results = results.reverse().reduce((acc, result) => { - let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(','); - if (!seen.has(resultStr)) { - acc.push(result); - seen.add(resultStr); - } - return acc; - }, []); - - return results.reverse(); - }, - - query: (query) => { - const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query); - const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms); - - // for debugging - //Search.lastresults = results.slice(); // a copy - // console.info("search results:", Search.lastresults); - - // print the results - _displayNextItem(results, results.length, searchTerms, highlightTerms); - }, - - /** - * search for object names - */ - performObjectSearch: (object, objectTerms) => { - const filenames = Search._index.filenames; - const docNames = Search._index.docnames; - const objects = Search._index.objects; - const objNames = Search._index.objnames; - const titles = Search._index.titles; - - const results = []; - - const objectSearchCallback = (prefix, match) => { - const name = match[4] - const fullname = (prefix ? prefix + "." : "") + name; - const fullnameLower = fullname.toLowerCase(); - if (fullnameLower.indexOf(object) < 0) return; - - let score = 0; - const parts = fullnameLower.split("."); - - // check for different match types: exact matches of full name or - // "last name" (i.e. last dotted part) - if (fullnameLower === object || parts.slice(-1)[0] === object) - score += Scorer.objNameMatch; - else if (parts.slice(-1)[0].indexOf(object) > -1) - score += Scorer.objPartialMatch; // matches in last name - - const objName = objNames[match[1]][2]; - const title = titles[match[0]]; - - // If more than one term searched for, we require other words to be - // found in the name/title/description - const otherTerms = new Set(objectTerms); - otherTerms.delete(object); - if (otherTerms.size > 0) { - const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase(); - if ( - [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0) - ) - return; - } - - let anchor = match[3]; - if (anchor === "") anchor = fullname; - else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname; - - const descr = objName + _(", in ") + title; - - // add custom score for some objects according to scorer - if (Scorer.objPrio.hasOwnProperty(match[2])) - score += Scorer.objPrio[match[2]]; - else score += Scorer.objPrioDefault; - - results.push([ - docNames[match[0]], - fullname, - "#" + anchor, - descr, - score, - filenames[match[0]], - ]); - }; - Object.keys(objects).forEach((prefix) => - objects[prefix].forEach((array) => - objectSearchCallback(prefix, array) - ) - ); - return results; - }, - - /** - * search for full-text terms in the index - */ - performTermsSearch: (searchTerms, excludedTerms) => { - // prepare search - const terms = Search._index.terms; - const titleTerms = Search._index.titleterms; - const filenames = Search._index.filenames; - const docNames = Search._index.docnames; - const titles = Search._index.titles; - - const scoreMap = new Map(); - const fileMap = new Map(); - - // perform the search on the required terms - searchTerms.forEach((word) => { - const files = []; - const arr = [ - { files: terms[word], score: Scorer.term }, - { files: titleTerms[word], score: Scorer.title }, - ]; - // add support for partial matches - if (word.length > 2) { - const escapedWord = _escapeRegExp(word); - if (!terms.hasOwnProperty(word)) { - Object.keys(terms).forEach((term) => { - if (term.match(escapedWord)) - arr.push({ files: terms[term], score: Scorer.partialTerm }); - }); - } - if (!titleTerms.hasOwnProperty(word)) { - Object.keys(titleTerms).forEach((term) => { - if (term.match(escapedWord)) - arr.push({ files: titleTerms[term], score: Scorer.partialTitle }); - }); - } - } - - // no match but word was a required one - if (arr.every((record) => record.files === undefined)) return; - - // found search word in contents - arr.forEach((record) => { - if (record.files === undefined) return; - - let recordFiles = record.files; - if (recordFiles.length === undefined) recordFiles = [recordFiles]; - files.push(...recordFiles); - - // set score for the word in each file - recordFiles.forEach((file) => { - if (!scoreMap.has(file)) scoreMap.set(file, {}); - scoreMap.get(file)[word] = record.score; - }); - }); - - // create the mapping - files.forEach((file) => { - if (!fileMap.has(file)) fileMap.set(file, [word]); - else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word); - }); - }); - - // now check if the files don't contain excluded terms - const results = []; - for (const [file, wordList] of fileMap) { - // check if all requirements are matched - - // as search terms with length < 3 are discarded - const filteredTermCount = [...searchTerms].filter( - (term) => term.length > 2 - ).length; - if ( - wordList.length !== searchTerms.size && - wordList.length !== filteredTermCount - ) - continue; - - // ensure that none of the excluded terms is in the search result - if ( - [...excludedTerms].some( - (term) => - terms[term] === file || - titleTerms[term] === file || - (terms[term] || []).includes(file) || - (titleTerms[term] || []).includes(file) - ) - ) - break; - - // select one (max) score for the file. - const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w])); - // add result to the result list - results.push([ - docNames[file], - titles[file], - "", - null, - score, - filenames[file], - ]); - } - return results; - }, - - /** - * helper function to return a node containing the - * search summary for a given text. keywords is a list - * of stemmed words. - */ - makeSearchSummary: (htmlText, keywords, anchor) => { - const text = Search.htmlToText(htmlText, anchor); - if (text === "") return null; - - const textLower = text.toLowerCase(); - const actualStartPosition = [...keywords] - .map((k) => textLower.indexOf(k.toLowerCase())) - .filter((i) => i > -1) - .slice(-1)[0]; - const startWithContext = Math.max(actualStartPosition - 120, 0); - - const top = startWithContext === 0 ? "" : "..."; - const tail = startWithContext + 240 < text.length ? "..." : ""; - - let summary = document.createElement("p"); - summary.classList.add("context"); - summary.textContent = top + text.substr(startWithContext, 240).trim() + tail; - - return summary; - }, -}; - -_ready(Search.init); +/* + * searchtools.js + * ~~~~~~~~~~~~~~~~ + * + * Sphinx JavaScript utilities for the full-text search. + * + * :copyright: Copyright 2007-2024 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ +"use strict"; + +/** + * Simple result scoring code. + */ +if (typeof Scorer === "undefined") { + var Scorer = { + // Implement the following function to further tweak the score for each result + // The function takes a result array [docname, title, anchor, descr, score, filename] + // and returns the new score. + /* + score: result => { + const [docname, title, anchor, descr, score, filename] = result + return score + }, + */ + + // query matches the full name of an object + objNameMatch: 11, + // or matches in the last dotted part of the object name + objPartialMatch: 6, + // Additive scores depending on the priority of the object + objPrio: { + 0: 15, // used to be importantResults + 1: 5, // used to be objectResults + 2: -5, // used to be unimportantResults + }, + // Used when the priority is not in the mapping. + objPrioDefault: 0, + + // query found in title + title: 15, + partialTitle: 7, + // query found in terms + term: 5, + partialTerm: 2, + }; +} + +const _removeChildren = (element) => { + while (element && element.lastChild) element.removeChild(element.lastChild); +}; + +/** + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + */ +const _escapeRegExp = (string) => + string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string + +const _displayItem = (item, searchTerms, highlightTerms) => { + const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; + const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; + const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX; + const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; + const contentRoot = document.documentElement.dataset.content_root; + + const [docName, title, anchor, descr, score, _filename] = item; + + let listItem = document.createElement("li"); + let requestUrl; + let linkUrl; + if (docBuilder === "dirhtml") { + // dirhtml builder + let dirname = docName + "/"; + if (dirname.match(/\/index\/$/)) + dirname = dirname.substring(0, dirname.length - 6); + else if (dirname === "index/") dirname = ""; + requestUrl = contentRoot + dirname; + linkUrl = requestUrl; + } else { + // normal html builders + requestUrl = contentRoot + docName + docFileSuffix; + linkUrl = docName + docLinkSuffix; + } + let linkEl = listItem.appendChild(document.createElement("a")); + linkEl.href = linkUrl + anchor; + linkEl.dataset.score = score; + linkEl.innerHTML = title; + if (descr) { + listItem.appendChild(document.createElement("span")).innerHTML = + " (" + descr + ")"; + // highlight search terms in the description + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + } + else if (showSearchSummary) + fetch(requestUrl) + .then((responseData) => responseData.text()) + .then((data) => { + if (data) + listItem.appendChild( + Search.makeSearchSummary(data, searchTerms, anchor) + ); + // highlight search terms in the summary + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + }); + Search.output.appendChild(listItem); +}; +const _finishSearch = (resultCount) => { + Search.stopPulse(); + Search.title.innerText = _("Search Results"); + if (!resultCount) + Search.status.innerText = Documentation.gettext( + "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." + ); + else + Search.status.innerText = _( + "Search finished, found ${resultCount} page(s) matching the search query." + ).replace('${resultCount}', resultCount); +}; +const _displayNextItem = ( + results, + resultCount, + searchTerms, + highlightTerms, +) => { + // results left, load the summary and display it + // this is intended to be dynamic (don't sub resultsCount) + if (results.length) { + _displayItem(results.pop(), searchTerms, highlightTerms); + setTimeout( + () => _displayNextItem(results, resultCount, searchTerms, highlightTerms), + 5 + ); + } + // search finished, update title and status message + else _finishSearch(resultCount); +}; +// Helper function used by query() to order search results. +// Each input is an array of [docname, title, anchor, descr, score, filename]. +// Order the results by score (in opposite order of appearance, since the +// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically. +const _orderResultsByScoreThenName = (a, b) => { + const leftScore = a[4]; + const rightScore = b[4]; + if (leftScore === rightScore) { + // same score: sort alphabetically + const leftTitle = a[1].toLowerCase(); + const rightTitle = b[1].toLowerCase(); + if (leftTitle === rightTitle) return 0; + return leftTitle > rightTitle ? -1 : 1; // inverted is intentional + } + return leftScore > rightScore ? 1 : -1; +}; + +/** + * Default splitQuery function. Can be overridden in ``sphinx.search`` with a + * custom function per language. + * + * The regular expression works by splitting the string on consecutive characters + * that are not Unicode letters, numbers, underscores, or emoji characters. + * This is the same as ``\W+`` in Python, preserving the surrogate pair area. + */ +if (typeof splitQuery === "undefined") { + var splitQuery = (query) => query + .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) + .filter(term => term) // remove remaining empty strings +} + +/** + * Search Module + */ +const Search = { + _index: null, + _queued_query: null, + _pulse_status: -1, + + htmlToText: (htmlString, anchor) => { + const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); + for (const removalQuery of [".headerlinks", "script", "style"]) { + htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() }); + } + if (anchor) { + const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`); + if (anchorContent) return anchorContent.textContent; + + console.warn( + `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.` + ); + } + + // if anchor not specified or not found, fall back to main content + const docContent = htmlElement.querySelector('[role="main"]'); + if (docContent) return docContent.textContent; + + console.warn( + "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template." + ); + return ""; + }, + + init: () => { + const query = new URLSearchParams(window.location.search).get("q"); + document + .querySelectorAll('input[name="q"]') + .forEach((el) => (el.value = query)); + if (query) Search.performSearch(query); + }, + + loadIndex: (url) => + (document.body.appendChild(document.createElement("script")).src = url), + + setIndex: (index) => { + Search._index = index; + if (Search._queued_query !== null) { + const query = Search._queued_query; + Search._queued_query = null; + Search.query(query); + } + }, + + hasIndex: () => Search._index !== null, + + deferQuery: (query) => (Search._queued_query = query), + + stopPulse: () => (Search._pulse_status = -1), + + startPulse: () => { + if (Search._pulse_status >= 0) return; + + const pulse = () => { + Search._pulse_status = (Search._pulse_status + 1) % 4; + Search.dots.innerText = ".".repeat(Search._pulse_status); + if (Search._pulse_status >= 0) window.setTimeout(pulse, 500); + }; + pulse(); + }, + + /** + * perform a search for something (or wait until index is loaded) + */ + performSearch: (query) => { + // create the required interface elements + const searchText = document.createElement("h2"); + searchText.textContent = _("Searching"); + const searchSummary = document.createElement("p"); + searchSummary.classList.add("search-summary"); + searchSummary.innerText = ""; + const searchList = document.createElement("ul"); + searchList.classList.add("search"); + + const out = document.getElementById("search-results"); + Search.title = out.appendChild(searchText); + Search.dots = Search.title.appendChild(document.createElement("span")); + Search.status = out.appendChild(searchSummary); + Search.output = out.appendChild(searchList); + + const searchProgress = document.getElementById("search-progress"); + // Some themes don't use the search progress node + if (searchProgress) { + searchProgress.innerText = _("Preparing search..."); + } + Search.startPulse(); + + // index already loaded, the browser was quick! + if (Search.hasIndex()) Search.query(query); + else Search.deferQuery(query); + }, + + _parseQuery: (query) => { + // stem the search terms and add them to the correct list + const stemmer = new Stemmer(); + const searchTerms = new Set(); + const excludedTerms = new Set(); + const highlightTerms = new Set(); + const objectTerms = new Set(splitQuery(query.toLowerCase().trim())); + splitQuery(query.trim()).forEach((queryTerm) => { + const queryTermLower = queryTerm.toLowerCase(); + + // maybe skip this "word" + // stopwords array is from language_data.js + if ( + stopwords.indexOf(queryTermLower) !== -1 || + queryTerm.match(/^\d+$/) + ) + return; + + // stem the word + let word = stemmer.stemWord(queryTermLower); + // select the correct list + if (word[0] === "-") excludedTerms.add(word.substr(1)); + else { + searchTerms.add(word); + highlightTerms.add(queryTermLower); + } + }); + + if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js + localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" ")) + } + + // console.debug("SEARCH: searching for:"); + // console.info("required: ", [...searchTerms]); + // console.info("excluded: ", [...excludedTerms]); + + return [query, searchTerms, excludedTerms, highlightTerms, objectTerms]; + }, + + /** + * execute search (requires search index to be loaded) + */ + _performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + const allTitles = Search._index.alltitles; + const indexEntries = Search._index.indexentries; + + // Collect multiple result groups to be sorted separately and then ordered. + // Each is an array of [docname, title, anchor, descr, score, filename]. + const normalResults = []; + const nonMainIndexResults = []; + + _removeChildren(document.getElementById("search-progress")); + + const queryLower = query.toLowerCase().trim(); + for (const [title, foundTitles] of Object.entries(allTitles)) { + if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { + for (const [file, id] of foundTitles) { + let score = Math.round(100 * queryLower.length / title.length) + normalResults.push([ + docNames[file], + titles[file] !== title ? `${titles[file]} > ${title}` : title, + id !== null ? "#" + id : "", + null, + score, + filenames[file], + ]); + } + } + } + + // search for explicit entries in index directives + for (const [entry, foundEntries] of Object.entries(indexEntries)) { + if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) { + for (const [file, id, isMain] of foundEntries) { + const score = Math.round(100 * queryLower.length / entry.length); + const result = [ + docNames[file], + titles[file], + id ? "#" + id : "", + null, + score, + filenames[file], + ]; + if (isMain) { + normalResults.push(result); + } else { + nonMainIndexResults.push(result); + } + } + } + } + + // lookup as object + objectTerms.forEach((term) => + normalResults.push(...Search.performObjectSearch(term, objectTerms)) + ); + + // lookup as search terms in fulltext + normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms)); + + // let the scorer override scores with a custom scoring function + if (Scorer.score) { + normalResults.forEach((item) => (item[4] = Scorer.score(item))); + nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item))); + } + + // Sort each group of results by score and then alphabetically by name. + normalResults.sort(_orderResultsByScoreThenName); + nonMainIndexResults.sort(_orderResultsByScoreThenName); + + // Combine the result groups in (reverse) order. + // Non-main index entries are typically arbitrary cross-references, + // so display them after other results. + let results = [...nonMainIndexResults, ...normalResults]; + + // remove duplicate search results + // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept + let seen = new Set(); + results = results.reverse().reduce((acc, result) => { + let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(','); + if (!seen.has(resultStr)) { + acc.push(result); + seen.add(resultStr); + } + return acc; + }, []); + + return results.reverse(); + }, + + query: (query) => { + const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query); + const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms); + + // for debugging + //Search.lastresults = results.slice(); // a copy + // console.info("search results:", Search.lastresults); + + // print the results + _displayNextItem(results, results.length, searchTerms, highlightTerms); + }, + + /** + * search for object names + */ + performObjectSearch: (object, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const objects = Search._index.objects; + const objNames = Search._index.objnames; + const titles = Search._index.titles; + + const results = []; + + const objectSearchCallback = (prefix, match) => { + const name = match[4] + const fullname = (prefix ? prefix + "." : "") + name; + const fullnameLower = fullname.toLowerCase(); + if (fullnameLower.indexOf(object) < 0) return; + + let score = 0; + const parts = fullnameLower.split("."); + + // check for different match types: exact matches of full name or + // "last name" (i.e. last dotted part) + if (fullnameLower === object || parts.slice(-1)[0] === object) + score += Scorer.objNameMatch; + else if (parts.slice(-1)[0].indexOf(object) > -1) + score += Scorer.objPartialMatch; // matches in last name + + const objName = objNames[match[1]][2]; + const title = titles[match[0]]; + + // If more than one term searched for, we require other words to be + // found in the name/title/description + const otherTerms = new Set(objectTerms); + otherTerms.delete(object); + if (otherTerms.size > 0) { + const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase(); + if ( + [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0) + ) + return; + } + + let anchor = match[3]; + if (anchor === "") anchor = fullname; + else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname; + + const descr = objName + _(", in ") + title; + + // add custom score for some objects according to scorer + if (Scorer.objPrio.hasOwnProperty(match[2])) + score += Scorer.objPrio[match[2]]; + else score += Scorer.objPrioDefault; + + results.push([ + docNames[match[0]], + fullname, + "#" + anchor, + descr, + score, + filenames[match[0]], + ]); + }; + Object.keys(objects).forEach((prefix) => + objects[prefix].forEach((array) => + objectSearchCallback(prefix, array) + ) + ); + return results; + }, + + /** + * search for full-text terms in the index + */ + performTermsSearch: (searchTerms, excludedTerms) => { + // prepare search + const terms = Search._index.terms; + const titleTerms = Search._index.titleterms; + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + + const scoreMap = new Map(); + const fileMap = new Map(); + + // perform the search on the required terms + searchTerms.forEach((word) => { + const files = []; + const arr = [ + { files: terms[word], score: Scorer.term }, + { files: titleTerms[word], score: Scorer.title }, + ]; + // add support for partial matches + if (word.length > 2) { + const escapedWord = _escapeRegExp(word); + if (!terms.hasOwnProperty(word)) { + Object.keys(terms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: terms[term], score: Scorer.partialTerm }); + }); + } + if (!titleTerms.hasOwnProperty(word)) { + Object.keys(titleTerms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: titleTerms[term], score: Scorer.partialTitle }); + }); + } + } + + // no match but word was a required one + if (arr.every((record) => record.files === undefined)) return; + + // found search word in contents + arr.forEach((record) => { + if (record.files === undefined) return; + + let recordFiles = record.files; + if (recordFiles.length === undefined) recordFiles = [recordFiles]; + files.push(...recordFiles); + + // set score for the word in each file + recordFiles.forEach((file) => { + if (!scoreMap.has(file)) scoreMap.set(file, {}); + scoreMap.get(file)[word] = record.score; + }); + }); + + // create the mapping + files.forEach((file) => { + if (!fileMap.has(file)) fileMap.set(file, [word]); + else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word); + }); + }); + + // now check if the files don't contain excluded terms + const results = []; + for (const [file, wordList] of fileMap) { + // check if all requirements are matched + + // as search terms with length < 3 are discarded + const filteredTermCount = [...searchTerms].filter( + (term) => term.length > 2 + ).length; + if ( + wordList.length !== searchTerms.size && + wordList.length !== filteredTermCount + ) + continue; + + // ensure that none of the excluded terms is in the search result + if ( + [...excludedTerms].some( + (term) => + terms[term] === file || + titleTerms[term] === file || + (terms[term] || []).includes(file) || + (titleTerms[term] || []).includes(file) + ) + ) + break; + + // select one (max) score for the file. + const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w])); + // add result to the result list + results.push([ + docNames[file], + titles[file], + "", + null, + score, + filenames[file], + ]); + } + return results; + }, + + /** + * helper function to return a node containing the + * search summary for a given text. keywords is a list + * of stemmed words. + */ + makeSearchSummary: (htmlText, keywords, anchor) => { + const text = Search.htmlToText(htmlText, anchor); + if (text === "") return null; + + const textLower = text.toLowerCase(); + const actualStartPosition = [...keywords] + .map((k) => textLower.indexOf(k.toLowerCase())) + .filter((i) => i > -1) + .slice(-1)[0]; + const startWithContext = Math.max(actualStartPosition - 120, 0); + + const top = startWithContext === 0 ? "" : "..."; + const tail = startWithContext + 240 < text.length ? "..." : ""; + + let summary = document.createElement("p"); + summary.classList.add("context"); + summary.textContent = top + text.substr(startWithContext, 240).trim() + tail; + + return summary; + }, +}; + +_ready(Search.init); diff --git a/docs/build/html/_static/sphinx_highlight.js b/docs/build/html/_static/sphinx_highlight.js index 8a96c69..2268ff7 100644 --- a/docs/build/html/_static/sphinx_highlight.js +++ b/docs/build/html/_static/sphinx_highlight.js @@ -1,154 +1,154 @@ -/* Highlighting utilities for Sphinx HTML documentation. */ -"use strict"; - -const SPHINX_HIGHLIGHT_ENABLED = true - -/** - * highlight a given string on a node by wrapping it in - * span elements with the given class name. - */ -const _highlight = (node, addItems, text, className) => { - if (node.nodeType === Node.TEXT_NODE) { - const val = node.nodeValue; - const parent = node.parentNode; - const pos = val.toLowerCase().indexOf(text); - if ( - pos >= 0 && - !parent.classList.contains(className) && - !parent.classList.contains("nohighlight") - ) { - let span; - - const closestNode = parent.closest("body, svg, foreignObject"); - const isInSVG = closestNode && closestNode.matches("svg"); - if (isInSVG) { - span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); - } else { - span = document.createElement("span"); - span.classList.add(className); - } - - span.appendChild(document.createTextNode(val.substr(pos, text.length))); - const rest = document.createTextNode(val.substr(pos + text.length)); - parent.insertBefore( - span, - parent.insertBefore( - rest, - node.nextSibling - ) - ); - node.nodeValue = val.substr(0, pos); - /* There may be more occurrences of search term in this node. So call this - * function recursively on the remaining fragment. - */ - _highlight(rest, addItems, text, className); - - if (isInSVG) { - const rect = document.createElementNS( - "http://www.w3.org/2000/svg", - "rect" - ); - const bbox = parent.getBBox(); - rect.x.baseVal.value = bbox.x; - rect.y.baseVal.value = bbox.y; - rect.width.baseVal.value = bbox.width; - rect.height.baseVal.value = bbox.height; - rect.setAttribute("class", className); - addItems.push({ parent: parent, target: rect }); - } - } - } else if (node.matches && !node.matches("button, select, textarea")) { - node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); - } -}; -const _highlightText = (thisNode, text, className) => { - let addItems = []; - _highlight(thisNode, addItems, text, className); - addItems.forEach((obj) => - obj.parent.insertAdjacentElement("beforebegin", obj.target) - ); -}; - -/** - * Small JavaScript module for the documentation. - */ -const SphinxHighlight = { - - /** - * highlight the search words provided in localstorage in the text - */ - highlightSearchWords: () => { - if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight - - // get and clear terms from localstorage - const url = new URL(window.location); - const highlight = - localStorage.getItem("sphinx_highlight_terms") - || url.searchParams.get("highlight") - || ""; - localStorage.removeItem("sphinx_highlight_terms") - url.searchParams.delete("highlight"); - window.history.replaceState({}, "", url); - - // get individual terms from highlight string - const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); - if (terms.length === 0) return; // nothing to do - - // There should never be more than one element matching "div.body" - const divBody = document.querySelectorAll("div.body"); - const body = divBody.length ? divBody[0] : document.querySelector("body"); - window.setTimeout(() => { - terms.forEach((term) => _highlightText(body, term, "highlighted")); - }, 10); - - const searchBox = document.getElementById("searchbox"); - if (searchBox === null) return; - searchBox.appendChild( - document - .createRange() - .createContextualFragment( - '" - ) - ); - }, - - /** - * helper function to hide the search marks again - */ - hideSearchWords: () => { - document - .querySelectorAll("#searchbox .highlight-link") - .forEach((el) => el.remove()); - document - .querySelectorAll("span.highlighted") - .forEach((el) => el.classList.remove("highlighted")); - localStorage.removeItem("sphinx_highlight_terms") - }, - - initEscapeListener: () => { - // only install a listener if it is really needed - if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; - - document.addEventListener("keydown", (event) => { - // bail for input elements - if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; - // bail with special keys - if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; - if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { - SphinxHighlight.hideSearchWords(); - event.preventDefault(); - } - }); - }, -}; - -_ready(() => { - /* Do not call highlightSearchWords() when we are on the search page. - * It will highlight words from the *previous* search query. - */ - if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); - SphinxHighlight.initEscapeListener(); -}); +/* Highlighting utilities for Sphinx HTML documentation. */ +"use strict"; + +const SPHINX_HIGHLIGHT_ENABLED = true + +/** + * highlight a given string on a node by wrapping it in + * span elements with the given class name. + */ +const _highlight = (node, addItems, text, className) => { + if (node.nodeType === Node.TEXT_NODE) { + const val = node.nodeValue; + const parent = node.parentNode; + const pos = val.toLowerCase().indexOf(text); + if ( + pos >= 0 && + !parent.classList.contains(className) && + !parent.classList.contains("nohighlight") + ) { + let span; + + const closestNode = parent.closest("body, svg, foreignObject"); + const isInSVG = closestNode && closestNode.matches("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.classList.add(className); + } + + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + const rest = document.createTextNode(val.substr(pos + text.length)); + parent.insertBefore( + span, + parent.insertBefore( + rest, + node.nextSibling + ) + ); + node.nodeValue = val.substr(0, pos); + /* There may be more occurrences of search term in this node. So call this + * function recursively on the remaining fragment. + */ + _highlight(rest, addItems, text, className); + + if (isInSVG) { + const rect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect" + ); + const bbox = parent.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute("class", className); + addItems.push({ parent: parent, target: rect }); + } + } + } else if (node.matches && !node.matches("button, select, textarea")) { + node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); + } +}; +const _highlightText = (thisNode, text, className) => { + let addItems = []; + _highlight(thisNode, addItems, text, className); + addItems.forEach((obj) => + obj.parent.insertAdjacentElement("beforebegin", obj.target) + ); +}; + +/** + * Small JavaScript module for the documentation. + */ +const SphinxHighlight = { + + /** + * highlight the search words provided in localstorage in the text + */ + highlightSearchWords: () => { + if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight + + // get and clear terms from localstorage + const url = new URL(window.location); + const highlight = + localStorage.getItem("sphinx_highlight_terms") + || url.searchParams.get("highlight") + || ""; + localStorage.removeItem("sphinx_highlight_terms") + url.searchParams.delete("highlight"); + window.history.replaceState({}, "", url); + + // get individual terms from highlight string + const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); + if (terms.length === 0) return; // nothing to do + + // There should never be more than one element matching "div.body" + const divBody = document.querySelectorAll("div.body"); + const body = divBody.length ? divBody[0] : document.querySelector("body"); + window.setTimeout(() => { + terms.forEach((term) => _highlightText(body, term, "highlighted")); + }, 10); + + const searchBox = document.getElementById("searchbox"); + if (searchBox === null) return; + searchBox.appendChild( + document + .createRange() + .createContextualFragment( + '" + ) + ); + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords: () => { + document + .querySelectorAll("#searchbox .highlight-link") + .forEach((el) => el.remove()); + document + .querySelectorAll("span.highlighted") + .forEach((el) => el.classList.remove("highlighted")); + localStorage.removeItem("sphinx_highlight_terms") + }, + + initEscapeListener: () => { + // only install a listener if it is really needed + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; + if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { + SphinxHighlight.hideSearchWords(); + event.preventDefault(); + } + }); + }, +}; + +_ready(() => { + /* Do not call highlightSearchWords() when we are on the search page. + * It will highlight words from the *previous* search query. + */ + if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); + SphinxHighlight.initEscapeListener(); +}); diff --git a/docs/build/html/genindex.html b/docs/build/html/genindex.html index 3afeba2..2e1259c 100644 --- a/docs/build/html/genindex.html +++ b/docs/build/html/genindex.html @@ -1,409 +1,409 @@ - - - - - - Index — DIMA 1.0.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - -
  • -
  • -
-
-
-
-
- - -

Index

- -
- A - | C - | D - | E - | G - | H - | I - | L - | M - | N - | P - | R - | S - | T - | U - | V - -
-

A

- - - -
- -

C

- - - -
- -

D

- - - -
- -

E

- - - -
- -

G

- - - -
- -

H

- - -
- -

I

- - - -
- -

L

- - - -
- -

M

- - -
- -

N

- - -
    -
  • - notebooks - -
  • -
- -

P

- - - -
    -
  • - pipelines.data_integration - -
  • -
- -

R

- - - -
- -

S

- - - -
- -

T

- - -
- -

U

- - - -
- -

V

- - - -
    -
  • - visualization.hdf5_vis - -
  • -
- - - -
-
- -
-
-
-
- - - + + + + + + Index — DIMA 1.0.0 documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ + +

Index

+ +
+ A + | C + | D + | E + | G + | H + | I + | L + | M + | N + | P + | R + | S + | T + | U + | V + +
+

A

+ + + +
+ +

C

+ + + +
+ +

D

+ + + +
+ +

E

+ + + +
+ +

G

+ + + +
+ +

H

+ + +
+ +

I

+ + + +
+ +

L

+ + + +
+ +

M

+ + +
+ +

N

+ + +
    +
  • + notebooks + +
  • +
+ +

P

+ + + +
    +
  • + pipelines.data_integration + +
  • +
+ +

R

+ + + +
+ +

S

+ + + +
+ +

T

+ + +
+ +

U

+ + + +
+ +

V

+ + + +
    +
  • + visualization.hdf5_vis + +
  • +
+ + + +
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/docs/build/html/index.html b/docs/build/html/index.html index 5f1c624..74d9c28 100644 --- a/docs/build/html/index.html +++ b/docs/build/html/index.html @@ -1,178 +1,178 @@ - - - - - - - Welcome to DIMA’s documentation! — DIMA 1.0.0 documentation - - - - - - - - - - - - - - - - - - - -
- - -
- - -
-
- - - + + + + + + + Welcome to DIMA’s documentation! — DIMA 1.0.0 documentation + + + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+
+ + + \ No newline at end of file diff --git a/docs/build/html/modules/instruments.html b/docs/build/html/modules/instruments.html index cdb5490..c22d452 100644 --- a/docs/build/html/modules/instruments.html +++ b/docs/build/html/modules/instruments.html @@ -1,109 +1,109 @@ - - - - - - - <no title> — DIMA 1.0.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- - - -
-
- -
-
-
-
- - - + + + + + + + <no title> — DIMA 1.0.0 documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/docs/build/html/modules/notebooks.html b/docs/build/html/modules/notebooks.html index 970945a..4fbec00 100644 --- a/docs/build/html/modules/notebooks.html +++ b/docs/build/html/modules/notebooks.html @@ -1,111 +1,111 @@ - - - - - - - Notebooks — DIMA 1.0.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

Notebooks

-
- - -
-
- -
-
-
-
- - - + + + + + + + Notebooks — DIMA 1.0.0 documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Notebooks

+
+ + +
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/docs/build/html/modules/pipelines.html b/docs/build/html/modules/pipelines.html index 3604101..c280582 100644 --- a/docs/build/html/modules/pipelines.html +++ b/docs/build/html/modules/pipelines.html @@ -1,194 +1,194 @@ - - - - - - - Pipelines and workflows — DIMA 1.0.0 documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

Pipelines and workflows

-
-
-pipelines.data_integration.copy_subtree_and_create_hdf5(src, dst, select_dir_keywords, select_file_keywords, allowed_file_extensions, root_metadata_dict)[source]
-

Helper function to copy directory with constraints and create HDF5.

-
- -
-
-pipelines.data_integration.load_config_and_setup_logging(yaml_config_file_path, log_dir)[source]
-

Load YAML configuration file, set up logging, and validate required keys and datetime_steps.

-
- -
-
-pipelines.data_integration.run_pipeline(path_to_config_yamlFile, log_dir='logs/')[source]
-

Integrates data sources specified by the input configuration file into HDF5 files.

-
-
Parameters:

yaml_config_file_path (str): Path to the YAML configuration file. -log_dir (str): Directory to save the log file.

-
-
Returns:

list: List of Paths to the created HDF5 file(s).

-
-
-
- -
-
-pipelines.metadata_revision.count(hdf5_obj, yml_dict)[source]
-
- -
-
-pipelines.metadata_revision.load_yaml(review_yaml_file)[source]
-
- -
-
-pipelines.metadata_revision.update_hdf5_file_with_review(input_hdf5_file, review_yaml_file)[source]
-

Updates, appends, or deletes metadata attributes in an HDF5 file based on a provided YAML dictionary.

-
-

Parameters:

-
-
input_hdf5_filestr

Path to the HDF5 file.

-
-
yaml_dictdict

Dictionary specifying objects and their attributes with operations. Example format: -{

-
-
-
“object_name”: { “attributes”“attr_name”: { “value”: attr_value,
-
-

“delete”: true | false

-
-

}

-
-

}

-
-
-
-

}

-
-
-
-
- -
-
-pipelines.metadata_revision.validate_yaml_dict(input_hdf5_file, yaml_dict)[source]
-
- -
- - -
-
- -
-
-
-
- - - + + + + + + + Pipelines and workflows — DIMA 1.0.0 documentation + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Pipelines and workflows

+
+
+pipelines.data_integration.copy_subtree_and_create_hdf5(src, dst, select_dir_keywords, select_file_keywords, allowed_file_extensions, root_metadata_dict)[source]
+

Helper function to copy directory with constraints and create HDF5.

+
+ +
+
+pipelines.data_integration.load_config_and_setup_logging(yaml_config_file_path, log_dir)[source]
+

Load YAML configuration file, set up logging, and validate required keys and datetime_steps.

+
+ +
+
+pipelines.data_integration.run_pipeline(path_to_config_yamlFile, log_dir='logs/')[source]
+

Integrates data sources specified by the input configuration file into HDF5 files.

+
+
Parameters:

yaml_config_file_path (str): Path to the YAML configuration file. +log_dir (str): Directory to save the log file.

+
+
Returns:

list: List of Paths to the created HDF5 file(s).

+
+
+
+ +
+
+pipelines.metadata_revision.count(hdf5_obj, yml_dict)[source]
+
+ +
+
+pipelines.metadata_revision.load_yaml(review_yaml_file)[source]
+
+ +
+
+pipelines.metadata_revision.update_hdf5_file_with_review(input_hdf5_file, review_yaml_file)[source]
+

Updates, appends, or deletes metadata attributes in an HDF5 file based on a provided YAML dictionary.

+
+

Parameters:

+
+
input_hdf5_filestr

Path to the HDF5 file.

+
+
yaml_dictdict

Dictionary specifying objects and their attributes with operations. Example format: +{

+
+
+
“object_name”: { “attributes”“attr_name”: { “value”: attr_value,
+
+

“delete”: true | false

+
+

}

+
+

}

+
+
+
+

}

+
+
+
+
+ +
+
+pipelines.metadata_revision.validate_yaml_dict(input_hdf5_file, yaml_dict)[source]
+
+ +
+ + +
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/docs/build/html/modules/src.html b/docs/build/html/modules/src.html index cc1746f..dd1fd21 100644 --- a/docs/build/html/modules/src.html +++ b/docs/build/html/modules/src.html @@ -1,460 +1,460 @@ - - - - - - - HDF5 Data Operations — DIMA 1.0.0 documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

HDF5 Data Operations

-
-
-class src.hdf5_ops.HDF5DataOpsManager(file_path, mode='r+')[source]
-

Bases: object

-

A class to handle HDF5 fundamental middle level file operations to power data updates, metadata revision, and data analysis -with hdf5 files encoding multi-instrument experimental campaign data.

-
-

Parameters:

-
-
-
path_to_filestr

path/to/hdf5file.

-
-
modestr

‘r’ or ‘r+’ read or read/write mode only when file exists

-
-
-
-
-
-append_dataset(dataset_dict, group_name)[source]
-
- -
-
-append_metadata(obj_name, annotation_dict)[source]
-

Appends metadata attributes to the specified object (obj_name) based on the provided annotation_dict.

-

This method ensures that the provided metadata attributes do not overwrite any existing ones. If an attribute already exists, -a ValueError is raised. The function supports storing scalar values (int, float, str) and compound values such as dictionaries -that are converted into NumPy structured arrays before being added to the metadata.

-
-

Parameters:

-
-
obj_name: str

Path to the target object (dataset or group) within the HDF5 file.

-
-
annotation_dict: dict
-
A dictionary where the keys represent new attribute names (strings), and the values can be:
    -
  • Scalars: int, float, or str.

  • -
  • Compound values (dictionaries) for more complex metadata, which are converted to NumPy structured arrays.

  • -
-

Example of a compound value:

-
-
annotation_dict = {
-
“relative_humidity”: {

“value”: 65, -“units”: “percentage”, -“range”: “[0,100]”, -“definition”: “amount of water vapor present …”

-
-
-

}

-
-
-

}

-
-
-
-
-
-
- -
-
-delete_metadata(obj_name, annotation_dict)[source]
-

Deletes metadata attributes of the specified object (obj_name) based on the provided annotation_dict.

-
-

Parameters:

-
-
obj_name: str

Path to the target object (dataset or group) within the HDF5 file.

-
-
annotation_dict: dict

Dictionary where keys represent attribute names, and values should be dictionaries containing -{“delete”: True} to mark them for deletion.

-
-
-
-
-

Example:

-

annotation_dict = {“attr_to_be_deleted”: {“delete”: True}}

-
-
-

Behavior:

-
    -
  • Deletes the specified attributes from the object’s metadata if marked for deletion.

  • -
  • Issues a warning if the attribute is not found or not marked for deletion.

  • -
-
-
- -
-
-extract_and_load_dataset_metadata()[source]
-
- -
-
-extract_dataset_as_dataframe(dataset_name)[source]
-

returns a copy of the dataset content in the form of dataframe when possible or numpy array

-
- -
-
-get_metadata(obj_path)[source]
-

Get file attributes from object at path = obj_path. For example, -obj_path = ‘/’ will get root level attributes or metadata.

-
- -
-
-load_file_obj()[source]
-
- -
-
-reformat_datetime_column(dataset_name, column_name, src_format, desired_format='%Y-%m-%d %H:%M:%S.%f')[source]
-
- -
-
-rename_metadata(obj_name, renaming_map)[source]
-

Renames metadata attributes of the specified object (obj_name) based on the provided renaming_map.

-
-

Parameters:

-
-
obj_name: str

Path to the target object (dataset or group) within the HDF5 file.

-
-
renaming_map: dict

A dictionary where keys are current attribute names (strings), and values are the new attribute names (strings or byte strings) to rename to.

-
-
renaming_map = {

“old_attr_name”: “new_attr_name”, -“old_attr_2”: “new_attr_2”

-
-
-

}

-
-
-
-
- -
-
-unload_file_obj()[source]
-
- -
-
-update_file(path_to_append_dir)[source]
-
- -
-
-update_metadata(obj_name, annotation_dict)[source]
-

Updates the value of existing metadata attributes of the specified object (obj_name) based on the provided annotation_dict.

-

The function disregards non-existing attributes and suggests to use the append_metadata() method to include those in the metadata.

-
-

Parameters:

-
-
obj_namestr

Path to the target object (dataset or group) within the HDF5 file.

-
-
annotation_dict: dict
-
A dictionary where the keys represent existing attribute names (strings), and the values can be:
    -
  • Scalars: int, float, or str.

  • -
  • Compound values (dictionaries) for more complex metadata, which are converted to NumPy structured arrays.

  • -
-

Example of a compound value:

-
-
annotation_dict = {
-
“relative_humidity”: {

“value”: 65, -“units”: “percentage”, -“range”: “[0,100]”, -“definition”: “amount of water vapor present …”

-
-
-

}

-
-
-

}

-
-
-
-
-
-
- -
-
- -
-
-src.hdf5_ops.get_groups_at_a_level(file: File, level: str)[source]
-
- -
-
-src.hdf5_ops.get_parent_child_relationships(file: File)[source]
-
- -
-
-src.hdf5_ops.read_mtable_as_dataframe(filename)[source]
-

Reconstruct a MATLAB Table encoded in a .h5 file as a Pandas DataFrame.

-

This function reads a .h5 file containing a MATLAB Table and reconstructs it as a Pandas DataFrame. -The input .h5 file contains one group per row of the MATLAB Table. Each group stores the table’s -dataset-like variables as Datasets, while categorical and numerical variables are represented as -attributes of the respective group.

-

To ensure homogeneity of data columns, the DataFrame is constructed column-wise.

-
-

Parameters

-
-
filenamestr

The name of the .h5 file. This may include the file’s location and path information.

-
-
-
-
-

Returns

-
-
pd.DataFrame

The MATLAB Table reconstructed as a Pandas DataFrame.

-
-
-
-
- -
-
-src.hdf5_ops.serialize_metadata(input_filename_path, folder_depth: int = 4, output_format: str = 'yaml') str[source]
-

Serialize metadata from an HDF5 file into YAML or JSON format.

-
-

Parameters

-
-
input_filename_pathstr

The path to the input HDF5 file.

-
-
folder_depthint, optional

The folder depth to control how much of the HDF5 file hierarchy is traversed (default is 4).

-
-
output_formatstr, optional

The format to serialize the output, either ‘yaml’ or ‘json’ (default is ‘yaml’).

-
-
-
-
-

Returns

-
-
str

The output file path where the serialized metadata is stored (either .yaml or .json).

-
-
-
-
- -
-
-

HDF5 Writer

-
-
-src.hdf5_writer.create_hdf5_file_from_dataframe(ofilename, input_data, group_by_funcs: list, approach: str = None, extract_attrs_func=None)[source]
-

Creates an HDF5 file with hierarchical groups based on the specified grouping functions or columns.

-
-

Parameters:

-
-

ofilename (str): Path for the output HDF5 file. -input_data (pd.DataFrame or str): Input data as a DataFrame or a valid file system path. -group_by_funcs (list): List of callables or column names to define hierarchical grouping. -approach (str): Specifies the approach (‘top-down’ or ‘bottom-up’) for creating the HDF5 file. -extract_attrs_func (callable, optional): Function to extract additional attributes for HDF5 groups.

-
-
-
-

Returns:

-
-

None

-
-
-
- -
-
-src.hdf5_writer.create_hdf5_file_from_filesystem_path(path_to_input_directory: str, path_to_filenames_dict: dict = None, select_dir_keywords: list = [], root_metadata_dict: dict = {}, mode='w')[source]
-

Creates an .h5 file with name “output_filename” that preserves the directory tree (or folder structure) -of a given filesystem path.

-

The data integration capabilities are limited by our file reader, which can only access data from a list of -admissible file formats. These, however, can be extended. Directories are groups in the resulting HDF5 file. -Files are formatted as composite objects consisting of a group, file, and attributes.

-
-

Parameters

-
-
output_filenamestr

Name of the output HDF5 file.

-
-
path_to_input_directorystr

Path to root directory, specified with forward slashes, e.g., path/to/root.

-
-
path_to_filenames_dictdict, optional

A pre-processed dictionary where keys are directory paths on the input directory’s tree and values are lists of files. -If provided, ‘input_file_system_path’ is ignored.

-
-
select_dir_keywordslist
-
List of string elements to consider or select only directory paths that contain

a word in ‘select_dir_keywords’. When empty, all directory paths are considered -to be included in the HDF5 file group hierarchy.

-
-
-
-
root_metadata_dictdict

Metadata to include at the root level of the HDF5 file.

-
-
modestr

‘w’ create File, truncate if it exists, or ‘r+’ read/write, File must exists. By default, mode = “w”.

-
-
-
-
-

Returns

-
-
output_filenamestr

Path to the created HDF5 file.

-
-
-
-
- -
-
-src.hdf5_writer.save_processed_dataframe_to_hdf5(df, annotator, output_filename)[source]
-

Save processed dataframe columns with annotations to an HDF5 file.

-
-
Parameters:

df (pd.DataFrame): DataFrame containing processed time series. -annotator (): Annotator object with get_metadata method. -output_filename (str): Path to the source HDF5 file.

-
-
-
- -
- - -
-
- -
-
-
-
- - - + + + + + + + HDF5 Data Operations — DIMA 1.0.0 documentation + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

HDF5 Data Operations

+
+
+class src.hdf5_ops.HDF5DataOpsManager(file_path, mode='r+')[source]
+

Bases: object

+

A class to handle HDF5 fundamental middle level file operations to power data updates, metadata revision, and data analysis +with hdf5 files encoding multi-instrument experimental campaign data.

+
+

Parameters:

+
+
+
path_to_filestr

path/to/hdf5file.

+
+
modestr

‘r’ or ‘r+’ read or read/write mode only when file exists

+
+
+
+
+
+append_dataset(dataset_dict, group_name)[source]
+
+ +
+
+append_metadata(obj_name, annotation_dict)[source]
+

Appends metadata attributes to the specified object (obj_name) based on the provided annotation_dict.

+

This method ensures that the provided metadata attributes do not overwrite any existing ones. If an attribute already exists, +a ValueError is raised. The function supports storing scalar values (int, float, str) and compound values such as dictionaries +that are converted into NumPy structured arrays before being added to the metadata.

+
+

Parameters:

+
+
obj_name: str

Path to the target object (dataset or group) within the HDF5 file.

+
+
annotation_dict: dict
+
A dictionary where the keys represent new attribute names (strings), and the values can be:
    +
  • Scalars: int, float, or str.

  • +
  • Compound values (dictionaries) for more complex metadata, which are converted to NumPy structured arrays.

  • +
+

Example of a compound value:

+
+
annotation_dict = {
+
“relative_humidity”: {

“value”: 65, +“units”: “percentage”, +“range”: “[0,100]”, +“definition”: “amount of water vapor present …”

+
+
+

}

+
+
+

}

+
+
+
+
+
+
+ +
+
+delete_metadata(obj_name, annotation_dict)[source]
+

Deletes metadata attributes of the specified object (obj_name) based on the provided annotation_dict.

+
+

Parameters:

+
+
obj_name: str

Path to the target object (dataset or group) within the HDF5 file.

+
+
annotation_dict: dict

Dictionary where keys represent attribute names, and values should be dictionaries containing +{“delete”: True} to mark them for deletion.

+
+
+
+
+

Example:

+

annotation_dict = {“attr_to_be_deleted”: {“delete”: True}}

+
+
+

Behavior:

+
    +
  • Deletes the specified attributes from the object’s metadata if marked for deletion.

  • +
  • Issues a warning if the attribute is not found or not marked for deletion.

  • +
+
+
+ +
+
+extract_and_load_dataset_metadata()[source]
+
+ +
+
+extract_dataset_as_dataframe(dataset_name)[source]
+

returns a copy of the dataset content in the form of dataframe when possible or numpy array

+
+ +
+
+get_metadata(obj_path)[source]
+

Get file attributes from object at path = obj_path. For example, +obj_path = ‘/’ will get root level attributes or metadata.

+
+ +
+
+load_file_obj()[source]
+
+ +
+
+reformat_datetime_column(dataset_name, column_name, src_format, desired_format='%Y-%m-%d %H:%M:%S.%f')[source]
+
+ +
+
+rename_metadata(obj_name, renaming_map)[source]
+

Renames metadata attributes of the specified object (obj_name) based on the provided renaming_map.

+
+

Parameters:

+
+
obj_name: str

Path to the target object (dataset or group) within the HDF5 file.

+
+
renaming_map: dict

A dictionary where keys are current attribute names (strings), and values are the new attribute names (strings or byte strings) to rename to.

+
+
renaming_map = {

“old_attr_name”: “new_attr_name”, +“old_attr_2”: “new_attr_2”

+
+
+

}

+
+
+
+
+ +
+
+unload_file_obj()[source]
+
+ +
+
+update_file(path_to_append_dir)[source]
+
+ +
+
+update_metadata(obj_name, annotation_dict)[source]
+

Updates the value of existing metadata attributes of the specified object (obj_name) based on the provided annotation_dict.

+

The function disregards non-existing attributes and suggests to use the append_metadata() method to include those in the metadata.

+
+

Parameters:

+
+
obj_namestr

Path to the target object (dataset or group) within the HDF5 file.

+
+
annotation_dict: dict
+
A dictionary where the keys represent existing attribute names (strings), and the values can be:
    +
  • Scalars: int, float, or str.

  • +
  • Compound values (dictionaries) for more complex metadata, which are converted to NumPy structured arrays.

  • +
+

Example of a compound value:

+
+
annotation_dict = {
+
“relative_humidity”: {

“value”: 65, +“units”: “percentage”, +“range”: “[0,100]”, +“definition”: “amount of water vapor present …”

+
+
+

}

+
+
+

}

+
+
+
+
+
+
+ +
+
+ +
+
+src.hdf5_ops.get_groups_at_a_level(file: File, level: str)[source]
+
+ +
+
+src.hdf5_ops.get_parent_child_relationships(file: File)[source]
+
+ +
+
+src.hdf5_ops.read_mtable_as_dataframe(filename)[source]
+

Reconstruct a MATLAB Table encoded in a .h5 file as a Pandas DataFrame.

+

This function reads a .h5 file containing a MATLAB Table and reconstructs it as a Pandas DataFrame. +The input .h5 file contains one group per row of the MATLAB Table. Each group stores the table’s +dataset-like variables as Datasets, while categorical and numerical variables are represented as +attributes of the respective group.

+

To ensure homogeneity of data columns, the DataFrame is constructed column-wise.

+
+

Parameters

+
+
filenamestr

The name of the .h5 file. This may include the file’s location and path information.

+
+
+
+
+

Returns

+
+
pd.DataFrame

The MATLAB Table reconstructed as a Pandas DataFrame.

+
+
+
+
+ +
+
+src.hdf5_ops.serialize_metadata(input_filename_path, folder_depth: int = 4, output_format: str = 'yaml') str[source]
+

Serialize metadata from an HDF5 file into YAML or JSON format.

+
+

Parameters

+
+
input_filename_pathstr

The path to the input HDF5 file.

+
+
folder_depthint, optional

The folder depth to control how much of the HDF5 file hierarchy is traversed (default is 4).

+
+
output_formatstr, optional

The format to serialize the output, either ‘yaml’ or ‘json’ (default is ‘yaml’).

+
+
+
+
+

Returns

+
+
str

The output file path where the serialized metadata is stored (either .yaml or .json).

+
+
+
+
+ +
+
+

HDF5 Writer

+
+
+src.hdf5_writer.create_hdf5_file_from_dataframe(ofilename, input_data, group_by_funcs: list, approach: str = None, extract_attrs_func=None)[source]
+

Creates an HDF5 file with hierarchical groups based on the specified grouping functions or columns.

+
+

Parameters:

+
+

ofilename (str): Path for the output HDF5 file. +input_data (pd.DataFrame or str): Input data as a DataFrame or a valid file system path. +group_by_funcs (list): List of callables or column names to define hierarchical grouping. +approach (str): Specifies the approach (‘top-down’ or ‘bottom-up’) for creating the HDF5 file. +extract_attrs_func (callable, optional): Function to extract additional attributes for HDF5 groups.

+
+
+
+

Returns:

+
+

None

+
+
+
+ +
+
+src.hdf5_writer.create_hdf5_file_from_filesystem_path(path_to_input_directory: str, path_to_filenames_dict: dict = None, select_dir_keywords: list = [], root_metadata_dict: dict = {}, mode='w')[source]
+

Creates an .h5 file with name “output_filename” that preserves the directory tree (or folder structure) +of a given filesystem path.

+

The data integration capabilities are limited by our file reader, which can only access data from a list of +admissible file formats. These, however, can be extended. Directories are groups in the resulting HDF5 file. +Files are formatted as composite objects consisting of a group, file, and attributes.

+
+

Parameters

+
+
output_filenamestr

Name of the output HDF5 file.

+
+
path_to_input_directorystr

Path to root directory, specified with forward slashes, e.g., path/to/root.

+
+
path_to_filenames_dictdict, optional

A pre-processed dictionary where keys are directory paths on the input directory’s tree and values are lists of files. +If provided, ‘input_file_system_path’ is ignored.

+
+
select_dir_keywordslist
+
List of string elements to consider or select only directory paths that contain

a word in ‘select_dir_keywords’. When empty, all directory paths are considered +to be included in the HDF5 file group hierarchy.

+
+
+
+
root_metadata_dictdict

Metadata to include at the root level of the HDF5 file.

+
+
modestr

‘w’ create File, truncate if it exists, or ‘r+’ read/write, File must exists. By default, mode = “w”.

+
+
+
+
+

Returns

+
+
output_filenamestr

Path to the created HDF5 file.

+
+
+
+
+ +
+
+src.hdf5_writer.save_processed_dataframe_to_hdf5(df, annotator, output_filename)[source]
+

Save processed dataframe columns with annotations to an HDF5 file.

+
+
Parameters:

df (pd.DataFrame): DataFrame containing processed time series. +annotator (): Annotator object with get_metadata method. +output_filename (str): Path to the source HDF5 file.

+
+
+
+ +
+ + +
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/docs/build/html/modules/utils.html b/docs/build/html/modules/utils.html index 014ae4e..55b1adb 100644 --- a/docs/build/html/modules/utils.html +++ b/docs/build/html/modules/utils.html @@ -1,304 +1,304 @@ - - - - - - - Data Structure Conversion — DIMA 1.0.0 documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

Data Structure Conversion

-
-
-utils.g5505_utils.augment_with_filenumber(df)[source]
-
- -
-
-utils.g5505_utils.augment_with_filetype(df)[source]
-
- -
-
-utils.g5505_utils.convert_attrdict_to_np_structured_array(attr_value: dict)[source]
-

Converts a dictionary of attributes into a numpy structured array for HDF5 -compound type compatibility.

-

Each dictionary key is mapped to a field in the structured array, with the -data type (S) determined by the longest string representation of the values. -If the dictionary is empty, the function returns ‘missing’.

-
-

Parameters

-
-
attr_valuedict

Dictionary containing the attributes to be converted. Example: -attr_value = {

-
-

‘name’: ‘Temperature’, -‘unit’: ‘Celsius’, -‘value’: 23.5, -‘timestamp’: ‘2023-09-26 10:00’

-
-

}

-
-
-
-
-

Returns

-
-
new_attr_valuendarray or str

Numpy structured array with UTF-8 encoded fields. Returns ‘missing’ if -the input dictionary is empty.

-
-
-
-
- -
-
-utils.g5505_utils.convert_dataframe_to_np_structured_array(df: DataFrame)[source]
-
- -
-
-utils.g5505_utils.convert_string_to_bytes(input_list: list)[source]
-

Convert a list of strings into a numpy array with utf8-type entries.

-
-

Parameters

-

input_list (list) : list of string objects

-
-
-

Returns

-

input_array_bytes (ndarray): array of ut8-type entries.

-
-
- -
-
-utils.g5505_utils.copy_directory_with_contraints(input_dir_path, output_dir_path, select_dir_keywords=None, select_file_keywords=None, allowed_file_extensions=None, dry_run=False)[source]
-

Copies files from input_dir_path to output_dir_path based on specified constraints.

-
-

Parameters

-
-

input_dir_path (str): Path to the input directory. -output_dir_path (str): Path to the output directory. -select_dir_keywords (list): optional, List of keywords for selecting directories. -select_file_keywords (list): optional, List of keywords for selecting files. -allowed_file_extensions (list): optional, List of allowed file extensions.

-
-
-
-

Returns

-
-

path_to_files_dict (dict): dictionary mapping directory paths to lists of copied file names satisfying the constraints.

-
-
-
- -
-
-utils.g5505_utils.created_at(datetime_format='%Y-%m-%d %H:%M:%S')[source]
-
- -
-
-utils.g5505_utils.group_by_df_column(df, column_name: str)[source]
-

df (pandas.DataFrame): -column_name (str): column_name of df by which grouping operation will take place.

-
- -
-
-utils.g5505_utils.infer_units(column_name)[source]
-
- -
-
-utils.g5505_utils.is_callable_list(x: list)[source]
-
- -
-
-utils.g5505_utils.is_str_list(x: list)[source]
-
- -
-
-utils.g5505_utils.is_structured_array(attr_val)[source]
-
- -
-
-utils.g5505_utils.make_file_copy(source_file_path, output_folder_name: str = 'tmp_files')[source]
-
- -
-
-utils.g5505_utils.progressBar(count_value, total, suffix='')[source]
-
- -
-
-utils.g5505_utils.sanitize_dataframe(df: DataFrame) DataFrame[source]
-
- -
-
-utils.g5505_utils.setup_logging(log_dir, log_filename)[source]
-

Sets up logging to a specified directory and file.

-
-
Parameters:

log_dir (str): Directory to save the log file. -log_filename (str): Name of the log file.

-
-
-
- -
-
-utils.g5505_utils.split_sample_col_into_sample_and_data_quality_cols(input_data: DataFrame)[source]
-
- -
-
-utils.g5505_utils.to_serializable_dtype(value)[source]
-

Transform value’s dtype into YAML/JSON compatible dtype

-
-

Parameters

-
-
value_type_

_description_

-
-
-
-
-

Returns

-
-
_type_

_description_

-
-
-
-
- -
- - -
-
- -
-
-
-
- - - + + + + + + + Data Structure Conversion — DIMA 1.0.0 documentation + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Data Structure Conversion

+
+
+utils.g5505_utils.augment_with_filenumber(df)[source]
+
+ +
+
+utils.g5505_utils.augment_with_filetype(df)[source]
+
+ +
+
+utils.g5505_utils.convert_attrdict_to_np_structured_array(attr_value: dict)[source]
+

Converts a dictionary of attributes into a numpy structured array for HDF5 +compound type compatibility.

+

Each dictionary key is mapped to a field in the structured array, with the +data type (S) determined by the longest string representation of the values. +If the dictionary is empty, the function returns ‘missing’.

+
+

Parameters

+
+
attr_valuedict

Dictionary containing the attributes to be converted. Example: +attr_value = {

+
+

‘name’: ‘Temperature’, +‘unit’: ‘Celsius’, +‘value’: 23.5, +‘timestamp’: ‘2023-09-26 10:00’

+
+

}

+
+
+
+
+

Returns

+
+
new_attr_valuendarray or str

Numpy structured array with UTF-8 encoded fields. Returns ‘missing’ if +the input dictionary is empty.

+
+
+
+
+ +
+
+utils.g5505_utils.convert_dataframe_to_np_structured_array(df: DataFrame)[source]
+
+ +
+
+utils.g5505_utils.convert_string_to_bytes(input_list: list)[source]
+

Convert a list of strings into a numpy array with utf8-type entries.

+
+

Parameters

+

input_list (list) : list of string objects

+
+
+

Returns

+

input_array_bytes (ndarray): array of ut8-type entries.

+
+
+ +
+
+utils.g5505_utils.copy_directory_with_contraints(input_dir_path, output_dir_path, select_dir_keywords=None, select_file_keywords=None, allowed_file_extensions=None, dry_run=False)[source]
+

Copies files from input_dir_path to output_dir_path based on specified constraints.

+
+

Parameters

+
+

input_dir_path (str): Path to the input directory. +output_dir_path (str): Path to the output directory. +select_dir_keywords (list): optional, List of keywords for selecting directories. +select_file_keywords (list): optional, List of keywords for selecting files. +allowed_file_extensions (list): optional, List of allowed file extensions.

+
+
+
+

Returns

+
+

path_to_files_dict (dict): dictionary mapping directory paths to lists of copied file names satisfying the constraints.

+
+
+
+ +
+
+utils.g5505_utils.created_at(datetime_format='%Y-%m-%d %H:%M:%S')[source]
+
+ +
+
+utils.g5505_utils.group_by_df_column(df, column_name: str)[source]
+

df (pandas.DataFrame): +column_name (str): column_name of df by which grouping operation will take place.

+
+ +
+
+utils.g5505_utils.infer_units(column_name)[source]
+
+ +
+
+utils.g5505_utils.is_callable_list(x: list)[source]
+
+ +
+
+utils.g5505_utils.is_str_list(x: list)[source]
+
+ +
+
+utils.g5505_utils.is_structured_array(attr_val)[source]
+
+ +
+
+utils.g5505_utils.make_file_copy(source_file_path, output_folder_name: str = 'tmp_files')[source]
+
+ +
+
+utils.g5505_utils.progressBar(count_value, total, suffix='')[source]
+
+ +
+
+utils.g5505_utils.sanitize_dataframe(df: DataFrame) DataFrame[source]
+
+ +
+
+utils.g5505_utils.setup_logging(log_dir, log_filename)[source]
+

Sets up logging to a specified directory and file.

+
+
Parameters:

log_dir (str): Directory to save the log file. +log_filename (str): Name of the log file.

+
+
+
+ +
+
+utils.g5505_utils.split_sample_col_into_sample_and_data_quality_cols(input_data: DataFrame)[source]
+
+ +
+
+utils.g5505_utils.to_serializable_dtype(value)[source]
+

Transform value’s dtype into YAML/JSON compatible dtype

+
+

Parameters

+
+
value_type_

_description_

+
+
+
+
+

Returns

+
+
_type_

_description_

+
+
+
+
+ +
+ + +
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/docs/build/html/modules/vis.html b/docs/build/html/modules/vis.html index 99b4687..c8a27c7 100644 --- a/docs/build/html/modules/vis.html +++ b/docs/build/html/modules/vis.html @@ -1,127 +1,127 @@ - - - - - - - Data Visualization — DIMA 1.0.0 documentation - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

Data Visualization

-
-
-visualization.hdf5_vis.display_group_hierarchy_on_a_treemap(filename: str)[source]
-

filename (str): hdf5 file’s filename

-
- -
- - -
-
- -
-
-
-
- - - + + + + + + + Data Visualization — DIMA 1.0.0 documentation + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Data Visualization

+
+
+visualization.hdf5_vis.display_group_hierarchy_on_a_treemap(filename: str)[source]
+

filename (str): hdf5 file’s filename

+
+ +
+ + +
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/docs/build/html/notebooks/workflow_di.html b/docs/build/html/notebooks/workflow_di.html index 4b7ce67..b80e4cd 100644 --- a/docs/build/html/notebooks/workflow_di.html +++ b/docs/build/html/notebooks/workflow_di.html @@ -1,111 +1,111 @@ - - - - - - - Tutorial workflows — DIMA 1.0.0 documentation - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
- -
-
-
-
- -
-

Tutorial workflows

-
- - -
-
- -
-
-
-
- - - + + + + + + + Tutorial workflows — DIMA 1.0.0 documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Tutorial workflows

+
+ + +
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/docs/build/html/py-modindex.html b/docs/build/html/py-modindex.html index 7683a8a..5732ec8 100644 --- a/docs/build/html/py-modindex.html +++ b/docs/build/html/py-modindex.html @@ -1,197 +1,197 @@ - - - - - - Python Module Index — DIMA 1.0.0 documentation - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - -
  • -
  • -
-
-
-
-
- - -

Python Module Index

- -
- n | - p | - s | - u | - v -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 
- n
- notebooks -
 
- p
- pipelines -
    - pipelines.data_integration -
    - pipelines.metadata_revision -
 
- s
- src -
    - src.hdf5_ops -
    - src.hdf5_writer -
 
- u
- utils -
    - utils.g5505_utils -
 
- v
- visualization -
    - visualization.hdf5_vis -
- - -
-
- -
-
-
-
- - - + + + + + + Python Module Index — DIMA 1.0.0 documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ + +

Python Module Index

+ +
+ n | + p | + s | + u | + v +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 
+ n
+ notebooks +
 
+ p
+ pipelines +
    + pipelines.data_integration +
    + pipelines.metadata_revision +
 
+ s
+ src +
    + src.hdf5_ops +
    + src.hdf5_writer +
 
+ u
+ utils +
    + utils.g5505_utils +
 
+ v
+ visualization +
    + visualization.hdf5_vis +
+ + +
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/docs/build/html/search.html b/docs/build/html/search.html index 58f776c..857d2a4 100644 --- a/docs/build/html/search.html +++ b/docs/build/html/search.html @@ -1,128 +1,128 @@ - - - - - - Search — DIMA 1.0.0 documentation - - - - - - - - - - - - - - - - - - - - - -
- - -
- -
-
-
-
    -
  • - -
  • -
  • -
-
-
-
-
- - - - -
- -
- -
-
- -
-
-
-
- - - - - - - - + + + + + + Search — DIMA 1.0.0 documentation + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ + + + +
+ +
+ +
+
+ +
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/docs/make.bat b/docs/make.bat index dc1312a..747ffb7 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -1,35 +1,35 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.https://www.sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "" goto help - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/software_arquitecture_diagram.svg b/docs/software_arquitecture_diagram.svg index cbdf89c..42c99db 100644 --- a/docs/software_arquitecture_diagram.svg +++ b/docs/software_arquitecture_diagram.svg @@ -1,1288 +1,1288 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - ExperimentalCampaignProject(Internal) - - - ScienceProject 1(Internal) - ScienceProject 2(External) - - - - DIMAPackage(Public) - - - - - - - - - - has subproject - - - - information flow - - - - - has subproject - - - - information flow - - - - - - - - - - - - File StandarizationModuleinstruments/ - - Data StructureConversionutils/ - - - - - - - - HDF5 Writersrc/ - - - - - HDF5 Data Operationssrc/ - - - - - - Git Operationssrc/ - - - - - - - - - Metadata Rev.Pipelinepipelines/ - - - - Visualizationvisualization/ - - - File Standardization and Storage - - - Information Processing and Data Analysis - - - Data Management - - - - - Data IntegrationPipelinepipelines/ - ETL and Data Analysis Demosnotebooks/ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + ExperimentalCampaignProject(Internal) + + + ScienceProject 1(Internal) + ScienceProject 2(External) + + + + DIMAPackage(Public) + + + + + + + + + + has subproject + + + + information flow + + + + + has subproject + + + + information flow + + + + + + + + + + + + File StandarizationModuleinstruments/ + + Data StructureConversionutils/ + + + + + + + + HDF5 Writersrc/ + + + + + HDF5 Data Operationssrc/ + + + + + + Git Operationssrc/ + + + + + + + + + Metadata Rev.Pipelinepipelines/ + + + + Visualizationvisualization/ + + + File Standardization and Storage + + + Information Processing and Data Analysis + + + Data Management + + + + + Data IntegrationPipelinepipelines/ + ETL and Data Analysis Demosnotebooks/ + + diff --git a/docs/source/conf.py b/docs/source/conf.py index c9bd2cb..d50094c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,55 +1,55 @@ -# Configuration file for the Sphinx documentation builder. -# -# For the full list of built-in configuration values, see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -import os -import sys -sys.path.insert(0, os.path.abspath('../..')) -print(os.getcwd()) -#print(os.path.abspath('../..')) - -# -- Project information ----------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information - -project = 'DIMA' -copyright = '2024, JFFO' -author = 'JFFO' -release = '1.0.0' - -# -- General configuration --------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration - -extensions = [ - 'nbsphinx', # added for jupyter notebooks - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - 'sphinx.ext.viewcode', # This extension adds links to highlighted source code -] - -templates_path = ['_templates'] -exclude_patterns = [] - -# nbsphinx configuration options -nbsphinx_allow_errors = True # Continue through notebook execution errors -#nbsphinx_execute = 'always' # Execute notebooks before converting -nbsphinx_execute = 'never' # Execute notebooks before converting - -# If you want to include the content of the Jupyter notebook cells in the index -nbsphinx_prolog = """ -.. raw:: html - -
-""" - -# -- Options for HTML output ------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output - -html_theme = "sphinx_rtd_theme" -html_static_path = ['_static'] - - -#extensions = [ -# 'sphinx.ext.autodoc', -# 'sphinx.ext.napoleon', +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +import os +import sys +sys.path.insert(0, os.path.abspath('../..')) +print(os.getcwd()) +#print(os.path.abspath('../..')) + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'DIMA' +copyright = '2024, JFFO' +author = 'JFFO' +release = '1.0.0' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + 'nbsphinx', # added for jupyter notebooks + 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', + 'sphinx.ext.viewcode', # This extension adds links to highlighted source code +] + +templates_path = ['_templates'] +exclude_patterns = [] + +# nbsphinx configuration options +nbsphinx_allow_errors = True # Continue through notebook execution errors +#nbsphinx_execute = 'always' # Execute notebooks before converting +nbsphinx_execute = 'never' # Execute notebooks before converting + +# If you want to include the content of the Jupyter notebook cells in the index +nbsphinx_prolog = """ +.. raw:: html + +
+""" + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "sphinx_rtd_theme" +html_static_path = ['_static'] + + +#extensions = [ +# 'sphinx.ext.autodoc', +# 'sphinx.ext.napoleon', #] \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index aadb26a..35afb05 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,28 +1,28 @@ -.. DIMA documentation master file, created by - sphinx-quickstart on Wed Jul 10 15:50:06 2024. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to DIMA's documentation! -================================ - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - modules/src - - modules/pipelines - - modules/vis - - modules/utils - - modules/notebooks - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` +.. DIMA documentation master file, created by + sphinx-quickstart on Wed Jul 10 15:50:06 2024. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to DIMA's documentation! +================================ + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + modules/src + + modules/pipelines + + modules/vis + + modules/utils + + modules/notebooks + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/source/modules/notebooks.rst b/docs/source/modules/notebooks.rst index d96ad41..c198a44 100644 --- a/docs/source/modules/notebooks.rst +++ b/docs/source/modules/notebooks.rst @@ -1,7 +1,7 @@ -Notebooks -========================== - -.. automodule:: notebooks - :members: - :undoc-members: +Notebooks +========================== + +.. automodule:: notebooks + :members: + :undoc-members: :show-inheritance: \ No newline at end of file diff --git a/docs/source/modules/pipelines.rst b/docs/source/modules/pipelines.rst index 685d139..c28b099 100644 --- a/docs/source/modules/pipelines.rst +++ b/docs/source/modules/pipelines.rst @@ -1,12 +1,12 @@ -Pipelines and workflows -========================== - -.. automodule:: pipelines.data_integration - :members: - :undoc-members: - :show-inheritance: - -.. automodule:: pipelines.metadata_revision - :members: - :undoc-members: +Pipelines and workflows +========================== + +.. automodule:: pipelines.data_integration + :members: + :undoc-members: + :show-inheritance: + +.. automodule:: pipelines.metadata_revision + :members: + :undoc-members: :show-inheritance: \ No newline at end of file diff --git a/docs/source/modules/src.rst b/docs/source/modules/src.rst index ecc8e69..90982ac 100644 --- a/docs/source/modules/src.rst +++ b/docs/source/modules/src.rst @@ -1,18 +1,18 @@ -HDF5 Data Operations -========================== -.. automodule:: src.hdf5_ops - :members: - :undoc-members: - :show-inheritance: - - -HDF5 Writer -========================== - -.. automodule:: src.hdf5_writer - :members: - :undoc-members: - :show-inheritance: - - - +HDF5 Data Operations +========================== +.. automodule:: src.hdf5_ops + :members: + :undoc-members: + :show-inheritance: + + +HDF5 Writer +========================== + +.. automodule:: src.hdf5_writer + :members: + :undoc-members: + :show-inheritance: + + + diff --git a/docs/source/modules/utils.rst b/docs/source/modules/utils.rst index 036a2d3..b34dae9 100644 --- a/docs/source/modules/utils.rst +++ b/docs/source/modules/utils.rst @@ -1,7 +1,7 @@ -Data Structure Conversion -========================= - -.. automodule:: utils.g5505_utils - :members: - :undoc-members: +Data Structure Conversion +========================= + +.. automodule:: utils.g5505_utils + :members: + :undoc-members: :show-inheritance: \ No newline at end of file diff --git a/docs/source/modules/vis.rst b/docs/source/modules/vis.rst index 5a25e0f..98eed89 100644 --- a/docs/source/modules/vis.rst +++ b/docs/source/modules/vis.rst @@ -1,7 +1,7 @@ -Data Visualization -================== - -.. automodule:: visualization.hdf5_vis - :members: - :undoc-members: +Data Visualization +================== + +.. automodule:: visualization.hdf5_vis + :members: + :undoc-members: :show-inheritance: \ No newline at end of file diff --git a/environment.yml b/environment.yml index 2051f5c..d5aea65 100644 --- a/environment.yml +++ b/environment.yml @@ -1,21 +1,21 @@ -name: pyenv5505 -#prefix: ./envs/pyenv5505 # Custom output folder -channels: - - conda-forge - - defaults -dependencies: - - python=3.11 - - jupyter - - numpy - - h5py - - pandas - - matplotlib - - plotly=5.24 - - scipy - - sphinx - - pip - - pip: -# - h5py==3.10 - - pybis==1.35 - - igor2 - - ipykernel +name: pyenv5505 +#prefix: ./envs/pyenv5505 # Custom output folder +channels: + - conda-forge + - defaults +dependencies: + - python=3.11 + - jupyter + - numpy + - h5py + - pandas + - matplotlib + - plotly=5.24 + - scipy + - sphinx + - pip + - pip: +# - h5py==3.10 + - pybis==1.35 + - igor2 + - ipykernel diff --git a/input_files/data_integr_config_file_LI.yaml b/input_files/data_integr_config_file_LI.yaml index 9c14647..970310d 100644 --- a/input_files/data_integr_config_file_LI.yaml +++ b/input_files/data_integr_config_file_LI.yaml @@ -1,69 +1,69 @@ -# Path to the directory where raw data is stored -input_file_directory: '//fs101/5505/Data' - -# Path to directory where raw data is copied and converted to HDF5 format for local analysis. -output_file_directory: '../output_files/' - -# Project metadata for data lineage and provenance -project: 'Photoenhanced uptake of NO2 driven by Fe(III)-carboxylate' -contact: 'LuciaI' -group_id: '5505' - -# Experiment description -experiment: 'kinetic_flowtube_study' # 'beamtime', 'smog_chamber_study' -dataset_startdate: -dataset_enddate: -actris_level: '0' - -# Instrument folders containing raw data from the campaign -instrument_datafolder: - - 'Lopap' # Example instrument folder - - 'Humidity_Sensors' - - 'ICAD/HONO' - - 'ICAD/NO2' - - 'T200_NOx' - - 'T360U_CO2' - -# Data integration mode for HDF5 data ingestion -integration_mode: 'collection' # Options: 'single_experiment', 'collection' - -# Datetime markers for individual experiments -# Use the format YYYY-MM-DD HH-MM-SS -datetime_steps: - - '2022-02-11 00-00-00' - - '2022-03-14 00-00-00' - - '2022-03-18 00-00-00' - - '2022-03-25 00-00-00' - - '2022-03-29 00-00-00' - - '2022-04-11 00-00-00' - - '2022-04-29 00-00-00' - - '2022-05-16 00-00-00' - - '2022-05-30 00-00-00' - - '2022-06-10 00-00-00' - - '2022-06-14 00-00-00' - - '2022-06-15 00-00-00' - - '2022-07-15 00-00-00' - - '2022-11-18 00-00-00' - - '2022-11-22 00-00-00' - - '2022-12-01 00-00-00' - - '2022-12-02 00-00-00' - - '2023-05-05 00-00-00' - - '2023-05-09 00-00-00' - - '2023-05-11 00-00-00' - - '2023-05-16 00-00-00' - - '2023-05-23 00-00-00' - - '2023-05-25 00-00-00' - - '2023-05-30 00-00-00' - - '2023-05-31 00-00-00' - - '2023-06-01 00-00-00' - - '2023-06-06 00-00-00' - - '2023-06-09 00-00-00' - - '2023-06-13 00-00-00' - - '2023-06-16 00-00-00' - - '2023-06-20 00-00-00' - - '2023-06-22 00-00-00' - - '2023-06-27 00-00-00' - - '2023-06-28 00-00-00' - - '2023-06-29 00-00-00' - +# Path to the directory where raw data is stored +input_file_directory: '//fs101/5505/Data' + +# Path to directory where raw data is copied and converted to HDF5 format for local analysis. +output_file_directory: '../output_files/' + +# Project metadata for data lineage and provenance +project: 'Photoenhanced uptake of NO2 driven by Fe(III)-carboxylate' +contact: 'LuciaI' +group_id: '5505' + +# Experiment description +experiment: 'kinetic_flowtube_study' # 'beamtime', 'smog_chamber_study' +dataset_startdate: +dataset_enddate: +actris_level: '0' + +# Instrument folders containing raw data from the campaign +instrument_datafolder: + - 'Lopap' # Example instrument folder + - 'Humidity_Sensors' + - 'ICAD/HONO' + - 'ICAD/NO2' + - 'T200_NOx' + - 'T360U_CO2' + +# Data integration mode for HDF5 data ingestion +integration_mode: 'collection' # Options: 'single_experiment', 'collection' + +# Datetime markers for individual experiments +# Use the format YYYY-MM-DD HH-MM-SS +datetime_steps: + - '2022-02-11 00-00-00' + - '2022-03-14 00-00-00' + - '2022-03-18 00-00-00' + - '2022-03-25 00-00-00' + - '2022-03-29 00-00-00' + - '2022-04-11 00-00-00' + - '2022-04-29 00-00-00' + - '2022-05-16 00-00-00' + - '2022-05-30 00-00-00' + - '2022-06-10 00-00-00' + - '2022-06-14 00-00-00' + - '2022-06-15 00-00-00' + - '2022-07-15 00-00-00' + - '2022-11-18 00-00-00' + - '2022-11-22 00-00-00' + - '2022-12-01 00-00-00' + - '2022-12-02 00-00-00' + - '2023-05-05 00-00-00' + - '2023-05-09 00-00-00' + - '2023-05-11 00-00-00' + - '2023-05-16 00-00-00' + - '2023-05-23 00-00-00' + - '2023-05-25 00-00-00' + - '2023-05-30 00-00-00' + - '2023-05-31 00-00-00' + - '2023-06-01 00-00-00' + - '2023-06-06 00-00-00' + - '2023-06-09 00-00-00' + - '2023-06-13 00-00-00' + - '2023-06-16 00-00-00' + - '2023-06-20 00-00-00' + - '2023-06-22 00-00-00' + - '2023-06-27 00-00-00' + - '2023-06-28 00-00-00' + - '2023-06-29 00-00-00' + \ No newline at end of file diff --git a/input_files/data_integr_config_file_NG.yaml b/input_files/data_integr_config_file_NG.yaml index 27f39e2..eb37de3 100644 --- a/input_files/data_integr_config_file_NG.yaml +++ b/input_files/data_integr_config_file_NG.yaml @@ -1,32 +1,32 @@ -# Path to the directory where raw data is stored -input_file_directory: '//fs03/Iron_Sulphate' - -# Path to directory where raw data is copied and converted to HDF5 format for local analysis. -output_file_directory: 'output_files/' - -# Project metadata for data lineage and provenance -project: 'Fe SOA project' -contact: 'NatashaG' -group_id: '5505' - -# Experiment description -experiment: 'smog_chamber_study' # beamtime, smog_chamber, lab_experiment -dataset_startdate: -dataset_enddate: -actris_level: '0' - -# Instrument folders containing raw data from the campaign -instrument_datafolder: - - 'gas' # Example instrument folder - - 'smps' - - 'htof' - - 'ptr' - - 'ams' - -# Data integration mode for HDF5 data ingestion -integration_mode: 'single_experiment' # Options: 'single_experiment', 'collection' - -# Datetime markers for individual experiments -# Use the format YYYY-MM-DD HH-MM-SS -datetime_steps: +# Path to the directory where raw data is stored +input_file_directory: '//fs03/Iron_Sulphate' + +# Path to directory where raw data is copied and converted to HDF5 format for local analysis. +output_file_directory: 'output_files/' + +# Project metadata for data lineage and provenance +project: 'Fe SOA project' +contact: 'NatashaG' +group_id: '5505' + +# Experiment description +experiment: 'smog_chamber_study' # beamtime, smog_chamber, lab_experiment +dataset_startdate: +dataset_enddate: +actris_level: '0' + +# Instrument folders containing raw data from the campaign +instrument_datafolder: + - 'gas' # Example instrument folder + - 'smps' + - 'htof' + - 'ptr' + - 'ams' + +# Data integration mode for HDF5 data ingestion +integration_mode: 'single_experiment' # Options: 'single_experiment', 'collection' + +# Datetime markers for individual experiments +# Use the format YYYY-MM-DD HH-MM-SS +datetime_steps: - '2022-07-26 00-00-00' \ No newline at end of file diff --git a/input_files/data_integr_config_file_TBR.yaml b/input_files/data_integr_config_file_TBR.yaml index 8d4078c..357c3bb 100644 --- a/input_files/data_integr_config_file_TBR.yaml +++ b/input_files/data_integr_config_file_TBR.yaml @@ -1,35 +1,35 @@ -# Path to the directory where raw data is stored -input_file_directory: '//fs101/5505/People/Juan/TypicalBeamTime' - -# Path to directory where raw data is copied and converted to HDF5 format for local analysis. -output_file_directory: 'output_files/' - -# Project metadata for data lineage and provenance -project: 'Beamtime May 2024, Ice Napp' -contact: 'ThorstenBR' -group_id: '5505' - -# Experiment description -experiment: 'beamtime' # beamtime, smog_chamber, lab_experiment -dataset_startdate: '2023-09-22' -dataset_enddate: '2023-09-25' -actris_level: '0' - -institution : "PSI" -filename_format : "institution,experiment,contact" - -# Instrument folders containing raw data from the campaign -instrument_datafolder: - - 'NEXAFS' - - 'Notes' - - 'Pressure' - - 'Photos' - - 'RGA' - - 'SES' - -# Data integration mode for HDF5 data ingestion -integration_mode: 'collection' # Options: 'single_experiment', 'collection' - -# Datetime markers for individual experiments -# Use the format YYYY-MM-DD HH-MM-SS +# Path to the directory where raw data is stored +input_file_directory: '//fs101/5505/People/Juan/TypicalBeamTime' + +# Path to directory where raw data is copied and converted to HDF5 format for local analysis. +output_file_directory: 'output_files/' + +# Project metadata for data lineage and provenance +project: 'Beamtime May 2024, Ice Napp' +contact: 'ThorstenBR' +group_id: '5505' + +# Experiment description +experiment: 'beamtime' # beamtime, smog_chamber, lab_experiment +dataset_startdate: '2023-09-22' +dataset_enddate: '2023-09-25' +actris_level: '0' + +institution : "PSI" +filename_format : "institution,experiment,contact" + +# Instrument folders containing raw data from the campaign +instrument_datafolder: + - 'NEXAFS' + - 'Notes' + - 'Pressure' + - 'Photos' + - 'RGA' + - 'SES' + +# Data integration mode for HDF5 data ingestion +integration_mode: 'collection' # Options: 'single_experiment', 'collection' + +# Datetime markers for individual experiments +# Use the format YYYY-MM-DD HH-MM-SS datetime_steps: [] \ No newline at end of file diff --git a/instruments/dictionaries/ACSM_TOFWARE.yaml b/instruments/dictionaries/ACSM_TOFWARE.yaml index 6f26847..29437bd 100644 --- a/instruments/dictionaries/ACSM_TOFWARE.yaml +++ b/instruments/dictionaries/ACSM_TOFWARE.yaml @@ -1,47 +1,47 @@ -table_header: - t_start_Buf: - description: Start time of the buffer (includes milliseconds) - datetime_format: "%d.%m.%Y %H:%M:%S.%f" - data_type: 'datetime' - - t_base: - description: Base time of the measurement (without milliseconds) - datetime_format: "%d.%m.%Y %H:%M:%S" - data_type: 'datetime' - - tseries: - description: Time series reference (without milliseconds) - datetime_format: "%d.%m.%Y %H:%M:%S" - data_type: 'datetime' - -config_text_reader: - - table_header: - #txt: - - 't_base VaporizerTemp_C HeaterBias_V FlowRefWave FlowRate_mb FlowRate_ccs FilamentEmission_mA Detector_V AnalogInput06_V ABRefWave ABsamp ABCorrFact' - - 't_start_Buf,Chl_11000,NH4_11000,SO4_11000,NO3_11000,Org_11000,SO4_48_11000,SO4_62_11000,SO4_82_11000,SO4_81_11000,SO4_98_11000,NO3_30_11000,Org_60_11000,Org_43_11000,Org_44_11000' - #csv: - - "X4 X5 X6 X7 X8 X9 X10 X11 X12 X13 X14 X15 X16 X17 X18 X19 X20 X21 X22 X23 X24 X25 X26 X27 X28 X29 X30 X31 X32 X33 X34 X35 X36 X37 X38 X39 X40 X41 X42 X43 X44 X45 X46 X47 X48 X49 X50 X51 X52 X53 X54 X55 X56 X57 X58 X59 X60 X61 X62 X63 X64 X65 X66 X67 X68 X69 X70 X71 X72 X73 X74 X75 X76 X77 X78 X79 X80 X81 X82 X83 X84 X85 X86 X87 X88 X89 X90 X91 X92 X93 X94 X95 X96 X97 X98 X99 X100 X101 X102 X103 X104 X105 X106 X107 X108 X109 X110 X111 X112 X113 X114 X115 X116 X117 X118 X119 X120 X121 X122 X123 X124 X125 X126 X127 X128 X129 X130 X131 X132 X133 X134 X135 X136 X137 X138 X139 X140 X141 X142 X143 X144 X145 X146 X147 X148 X149 X150 X151 X152 X153 X154 X155 X156 X157 X158 X159 X160 X161 X162 X163 X164 X165 X166 X167 X168 X169 X170 X171 X172 X173 X174 X175 X176 X177 X178 X179 X180 X181 X182 X183 X184 X185 X186 X187 X188 X189 X190 X191 X192 X193 X194 X195 X196 X197 X198 X199 X200 X201 X202 X203 X204 X205 X206 X207 X208 X209 X210 X211 X212 X213 X214 X215 X216 X217 X218 X219" - - "X4 X5 X6 X7 X8 X9 X10 X11 X12 X13 X14 X15 X16 X17 X18 X19 X20 X21 X22 X23 X24 X25 X26 X27 X28 X29 X30 X31 X32 X33 X34 X35 X36 X37 X38 X39 X40 X41 X42 X43 X44 X45 X46 X47 X48 X49 X50 X51 X52 X53 X54 X55 X56 X57 X58 X59 X60 X61 X62 X63 X64 X65 X66 X67 X68 X69 X70 X71 X72 X73 X74 X75 X76 X77 X78 X79 X80 X81 X82 X83 X84 X85 X86 X87 X88 X89 X90 X91 X92 X93 X94 X95 X96 X97 X98 X99 X100 X101 X102 X103 X104 X105 X106 X107 X108 X109 X110 X111 X112 X113 X114 X115 X116 X117 X118 X119 X120 X121 X122 X123 X124 X125 X126 X127 X128 X129 X130 X131 X132 X133 X134 X135 X136 X137 X138 X139 X140 X141 X142 X143 X144 X145 X146 X147 X148 X149 X150 X151 X152 X153 X154 X155 X156 X157 X158 X159 X160 X161 X162 X163 X164 X165 X166 X167 X168 X169 X170 X171 X172 X173 X174 X175 X176 X177 X178 X179 X180 X181 X182 X183 X184 X185 X186 X187 X188 X189 X190 X191 X192 X193 X194 X195 X196 X197 X198 X199 X200 X201 X202 X203 X204 X205 X206 X207 X208 X209 X210 X211 X212 X213 X214 X215 X216 X217 X218 X219" - - 'MSS_base' - - 'tseries' - separator: - #txt: - - "\t" - - "," - #csv: - - "\t" - - "\t" - - "None" - - "None" - file_encoding: - #txt: - - "utf-8" - - "utf-8" - #csv: - - "utf-8" - - "utf-8" - - "utf-8" - - "utf-8" - - +table_header: + t_start_Buf: + description: Start time of the buffer (includes milliseconds) + datetime_format: "%d.%m.%Y %H:%M:%S.%f" + data_type: 'datetime' + + t_base: + description: Base time of the measurement (without milliseconds) + datetime_format: "%d.%m.%Y %H:%M:%S" + data_type: 'datetime' + + tseries: + description: Time series reference (without milliseconds) + datetime_format: "%d.%m.%Y %H:%M:%S" + data_type: 'datetime' + +config_text_reader: + + table_header: + #txt: + - 't_base VaporizerTemp_C HeaterBias_V FlowRefWave FlowRate_mb FlowRate_ccs FilamentEmission_mA Detector_V AnalogInput06_V ABRefWave ABsamp ABCorrFact' + - 't_start_Buf,Chl_11000,NH4_11000,SO4_11000,NO3_11000,Org_11000,SO4_48_11000,SO4_62_11000,SO4_82_11000,SO4_81_11000,SO4_98_11000,NO3_30_11000,Org_60_11000,Org_43_11000,Org_44_11000' + #csv: + - "X4 X5 X6 X7 X8 X9 X10 X11 X12 X13 X14 X15 X16 X17 X18 X19 X20 X21 X22 X23 X24 X25 X26 X27 X28 X29 X30 X31 X32 X33 X34 X35 X36 X37 X38 X39 X40 X41 X42 X43 X44 X45 X46 X47 X48 X49 X50 X51 X52 X53 X54 X55 X56 X57 X58 X59 X60 X61 X62 X63 X64 X65 X66 X67 X68 X69 X70 X71 X72 X73 X74 X75 X76 X77 X78 X79 X80 X81 X82 X83 X84 X85 X86 X87 X88 X89 X90 X91 X92 X93 X94 X95 X96 X97 X98 X99 X100 X101 X102 X103 X104 X105 X106 X107 X108 X109 X110 X111 X112 X113 X114 X115 X116 X117 X118 X119 X120 X121 X122 X123 X124 X125 X126 X127 X128 X129 X130 X131 X132 X133 X134 X135 X136 X137 X138 X139 X140 X141 X142 X143 X144 X145 X146 X147 X148 X149 X150 X151 X152 X153 X154 X155 X156 X157 X158 X159 X160 X161 X162 X163 X164 X165 X166 X167 X168 X169 X170 X171 X172 X173 X174 X175 X176 X177 X178 X179 X180 X181 X182 X183 X184 X185 X186 X187 X188 X189 X190 X191 X192 X193 X194 X195 X196 X197 X198 X199 X200 X201 X202 X203 X204 X205 X206 X207 X208 X209 X210 X211 X212 X213 X214 X215 X216 X217 X218 X219" + - "X4 X5 X6 X7 X8 X9 X10 X11 X12 X13 X14 X15 X16 X17 X18 X19 X20 X21 X22 X23 X24 X25 X26 X27 X28 X29 X30 X31 X32 X33 X34 X35 X36 X37 X38 X39 X40 X41 X42 X43 X44 X45 X46 X47 X48 X49 X50 X51 X52 X53 X54 X55 X56 X57 X58 X59 X60 X61 X62 X63 X64 X65 X66 X67 X68 X69 X70 X71 X72 X73 X74 X75 X76 X77 X78 X79 X80 X81 X82 X83 X84 X85 X86 X87 X88 X89 X90 X91 X92 X93 X94 X95 X96 X97 X98 X99 X100 X101 X102 X103 X104 X105 X106 X107 X108 X109 X110 X111 X112 X113 X114 X115 X116 X117 X118 X119 X120 X121 X122 X123 X124 X125 X126 X127 X128 X129 X130 X131 X132 X133 X134 X135 X136 X137 X138 X139 X140 X141 X142 X143 X144 X145 X146 X147 X148 X149 X150 X151 X152 X153 X154 X155 X156 X157 X158 X159 X160 X161 X162 X163 X164 X165 X166 X167 X168 X169 X170 X171 X172 X173 X174 X175 X176 X177 X178 X179 X180 X181 X182 X183 X184 X185 X186 X187 X188 X189 X190 X191 X192 X193 X194 X195 X196 X197 X198 X199 X200 X201 X202 X203 X204 X205 X206 X207 X208 X209 X210 X211 X212 X213 X214 X215 X216 X217 X218 X219" + - 'MSS_base' + - 'tseries' + separator: + #txt: + - "\t" + - "," + #csv: + - "\t" + - "\t" + - "None" + - "None" + file_encoding: + #txt: + - "utf-8" + - "utf-8" + #csv: + - "utf-8" + - "utf-8" + - "utf-8" + - "utf-8" + + diff --git a/instruments/dictionaries/Humidity_Sensors.yaml b/instruments/dictionaries/Humidity_Sensors.yaml index 9c07366..f87fea8 100644 --- a/instruments/dictionaries/Humidity_Sensors.yaml +++ b/instruments/dictionaries/Humidity_Sensors.yaml @@ -1,105 +1,105 @@ -table_header: - Date: - description: Date of the humidity measurement - units: YYYY-MM-DD - rename_as: date - Time: - description: Time of the humidity measurement - units: HH:MM:SS - rename_as: time - RH1[%]: - description: Relative humidity measured by sensor 1 - units: percent - rename_as: rh1 - RH2[%]: - description: Relative humidity measured by sensor 2 - units: percent - rename_as: rh2 - RH3[%]: - description: Relative humidity measured by sensor 3 - units: percent - rename_as: rh3 - RH4[%]: - description: Relative humidity measured by sensor 4 - units: percent - rename_as: rh4 - RH5[%]: - description: Relative humidity measured by sensor 5 - units: percent - rename_as: rh5 - RH6[%]: - description: Relative humidity measured by sensor 6 - units: percent - rename_as: rh6 - RH7[%]: - description: Relative humidity measured by sensor 7 - units: percent - rename_as: rh7 - RH8[%]: - description: Relative humidity measured by sensor 8 - units: percent - rename_as: rh8 - T1[°C]: - description: Temperature measured by sensor 1 - units: Celsius - rename_as: t1 - T2[°C]: - description: Temperature measured by sensor 2 - units: Celsius - rename_as: t2 - T3[°C]: - description: Temperature measured by sensor 3 - units: Celsius - rename_as: t3 - T4[°C]: - description: Temperature measured by sensor 4 - units: Celsius - rename_as: t4 - T5[°C]: - description: Temperature measured by sensor 5 - units: Celsius - rename_as: t5 - T6[°C]: - description: Temperature measured by sensor 6 - units: Celsius - rename_as: t6 - T7[°C]: - description: Temperature measured by sensor 7 - units: Celsius - rename_as: t7 - T8[°C]: - description: Temperature measured by sensor 8 - units: Celsius - rename_as: t8 - DP1[°C]: - description: Dew point measured by sensor 1 - units: Celsius - rename_as: dp1 - DP2[°C]: - description: Dew point measured by sensor 2 - units: Celsius - rename_as: dp2 - DP3[°C]: - description: Dew point measured by sensor 3 - units: Celsius - rename_as: dp3 - DP4[°C]: - description: Dew point measured by sensor 4 - units: Celsius - rename_as: dp4 - DP5[°C]: - description: Dew point measured by sensor 5 - units: Celsius - rename_as: dp5 - DP6[°C]: - description: Dew point measured by sensor 6 - units: Celsius - rename_as: dp6 - DP7[°C]: - description: Dew point measured by sensor 7 - units: Celsius - rename_as: dp7 - DP8[°C]: - description: Dew point measured by sensor 8 - units: Celsius - rename_as: dp8 +table_header: + Date: + description: Date of the humidity measurement + units: YYYY-MM-DD + rename_as: date + Time: + description: Time of the humidity measurement + units: HH:MM:SS + rename_as: time + RH1[%]: + description: Relative humidity measured by sensor 1 + units: percent + rename_as: rh1 + RH2[%]: + description: Relative humidity measured by sensor 2 + units: percent + rename_as: rh2 + RH3[%]: + description: Relative humidity measured by sensor 3 + units: percent + rename_as: rh3 + RH4[%]: + description: Relative humidity measured by sensor 4 + units: percent + rename_as: rh4 + RH5[%]: + description: Relative humidity measured by sensor 5 + units: percent + rename_as: rh5 + RH6[%]: + description: Relative humidity measured by sensor 6 + units: percent + rename_as: rh6 + RH7[%]: + description: Relative humidity measured by sensor 7 + units: percent + rename_as: rh7 + RH8[%]: + description: Relative humidity measured by sensor 8 + units: percent + rename_as: rh8 + T1[°C]: + description: Temperature measured by sensor 1 + units: Celsius + rename_as: t1 + T2[°C]: + description: Temperature measured by sensor 2 + units: Celsius + rename_as: t2 + T3[°C]: + description: Temperature measured by sensor 3 + units: Celsius + rename_as: t3 + T4[°C]: + description: Temperature measured by sensor 4 + units: Celsius + rename_as: t4 + T5[°C]: + description: Temperature measured by sensor 5 + units: Celsius + rename_as: t5 + T6[°C]: + description: Temperature measured by sensor 6 + units: Celsius + rename_as: t6 + T7[°C]: + description: Temperature measured by sensor 7 + units: Celsius + rename_as: t7 + T8[°C]: + description: Temperature measured by sensor 8 + units: Celsius + rename_as: t8 + DP1[°C]: + description: Dew point measured by sensor 1 + units: Celsius + rename_as: dp1 + DP2[°C]: + description: Dew point measured by sensor 2 + units: Celsius + rename_as: dp2 + DP3[°C]: + description: Dew point measured by sensor 3 + units: Celsius + rename_as: dp3 + DP4[°C]: + description: Dew point measured by sensor 4 + units: Celsius + rename_as: dp4 + DP5[°C]: + description: Dew point measured by sensor 5 + units: Celsius + rename_as: dp5 + DP6[°C]: + description: Dew point measured by sensor 6 + units: Celsius + rename_as: dp6 + DP7[°C]: + description: Dew point measured by sensor 7 + units: Celsius + rename_as: dp7 + DP8[°C]: + description: Dew point measured by sensor 8 + units: Celsius + rename_as: dp8 diff --git a/instruments/dictionaries/ICAD_HONO.yaml b/instruments/dictionaries/ICAD_HONO.yaml index bd18b90..868dce7 100644 --- a/instruments/dictionaries/ICAD_HONO.yaml +++ b/instruments/dictionaries/ICAD_HONO.yaml @@ -1,121 +1,121 @@ -table_header: - Start Date/Time (UTC): - description: Start date and time of the measurement in UTC - units: YYYY-MM-DD HH:MM:SS - rename_as: start_datetime_utc - Duration (s): - description: Duration of the measurement in seconds - units: seconds - rename_as: duration_seconds - NO2 (ppb): - description: NO2 concentration - units: ppb - rename_as: no2_concentration - NO2 Uncertainty (ppb): - description: Uncertainty in NO2 concentration - units: ppb - rename_as: no2_uncertainty - HONO (ppb): - description: HONO concentration - units: ppb - rename_as: hono_concentration - HONO Uncertainty (ppb): - description: Uncertainty in HONO concentration - units: ppb - rename_as: hono_uncertainty - H2O (ppb): - description: H2O concentration - units: ppb - rename_as: h2o_concentration - H2O Uncertainty (ppb): - description: Uncertainty in H2O concentration - units: ppb - rename_as: h2o_uncertainty - O4 (ppb): - description: O4 concentration - units: ppb - rename_as: o4_concentration - O4 Uncertainty (ppb): - description: Uncertainty in O4 concentration - units: ppb - rename_as: o4_uncertainty - File Number: - description: File number - units: unspecified - rename_as: file_number - Light Intensity: - description: Light intensity - units: unspecified - rename_as: light_intensity - 12_#ICEDOAS iter.: - description: Number of ICEDOAS iterations - units: unspecified - rename_as: icedoas_iterations - Cell Pressure: - description: Cell pressure - units: unspecified - rename_as: cell_pressure - Ambient Pressure: - description: Ambient pressure - units: unspecified - rename_as: ambient_pressure - Cell Temp: - description: Cell temperature - units: unspecified - rename_as: cell_temp - Spec Temp: - description: Spectrometer temperature - units: unspecified - rename_as: spec_temp - Lat: - description: Latitude - units: unspecified - rename_as: latitude - Lon: - description: Longitude - units: unspecified - rename_as: longitude - Height: - description: Height - units: unspecified - rename_as: height - Speed: - description: Speed - units: unspecified - rename_as: speed - GPSQuality: - description: GPS quality - units: unspecified - rename_as: gps_quality - 0-Air Ref. Time: - description: 0-air reference time - units: unspecified - rename_as: zero_air_ref_time - 0-Air Ref. Duration: - description: 0-air reference duration - units: unspecified - rename_as: zero_air_ref_duration - 0-Air Ref. File Number: - description: 0-air reference file number - units: unspecified - rename_as: zero_air_ref_file_number - 0-Air Ref. Intensity: - description: 0-air reference intensity - units: unspecified - rename_as: zero_air_ref_intensity - 0-Air Ref. Rel Intensity: - description: 0-air reference relative intensity - units: unspecified - rename_as: zero_air_ref_rel_intensity - 0-Air Ref. Intensity valid: - description: 0-air reference intensity validity - units: unspecified - rename_as: zero_air_ref_intensity_valid - MeasMode: - description: Measurement mode - units: unspecified - rename_as: measurement_mode - SampleSource: - description: Sample source - units: unspecified - rename_as: sample_source +table_header: + Start Date/Time (UTC): + description: Start date and time of the measurement in UTC + units: YYYY-MM-DD HH:MM:SS + rename_as: start_datetime_utc + Duration (s): + description: Duration of the measurement in seconds + units: seconds + rename_as: duration_seconds + NO2 (ppb): + description: NO2 concentration + units: ppb + rename_as: no2_concentration + NO2 Uncertainty (ppb): + description: Uncertainty in NO2 concentration + units: ppb + rename_as: no2_uncertainty + HONO (ppb): + description: HONO concentration + units: ppb + rename_as: hono_concentration + HONO Uncertainty (ppb): + description: Uncertainty in HONO concentration + units: ppb + rename_as: hono_uncertainty + H2O (ppb): + description: H2O concentration + units: ppb + rename_as: h2o_concentration + H2O Uncertainty (ppb): + description: Uncertainty in H2O concentration + units: ppb + rename_as: h2o_uncertainty + O4 (ppb): + description: O4 concentration + units: ppb + rename_as: o4_concentration + O4 Uncertainty (ppb): + description: Uncertainty in O4 concentration + units: ppb + rename_as: o4_uncertainty + File Number: + description: File number + units: unspecified + rename_as: file_number + Light Intensity: + description: Light intensity + units: unspecified + rename_as: light_intensity + 12_#ICEDOAS iter.: + description: Number of ICEDOAS iterations + units: unspecified + rename_as: icedoas_iterations + Cell Pressure: + description: Cell pressure + units: unspecified + rename_as: cell_pressure + Ambient Pressure: + description: Ambient pressure + units: unspecified + rename_as: ambient_pressure + Cell Temp: + description: Cell temperature + units: unspecified + rename_as: cell_temp + Spec Temp: + description: Spectrometer temperature + units: unspecified + rename_as: spec_temp + Lat: + description: Latitude + units: unspecified + rename_as: latitude + Lon: + description: Longitude + units: unspecified + rename_as: longitude + Height: + description: Height + units: unspecified + rename_as: height + Speed: + description: Speed + units: unspecified + rename_as: speed + GPSQuality: + description: GPS quality + units: unspecified + rename_as: gps_quality + 0-Air Ref. Time: + description: 0-air reference time + units: unspecified + rename_as: zero_air_ref_time + 0-Air Ref. Duration: + description: 0-air reference duration + units: unspecified + rename_as: zero_air_ref_duration + 0-Air Ref. File Number: + description: 0-air reference file number + units: unspecified + rename_as: zero_air_ref_file_number + 0-Air Ref. Intensity: + description: 0-air reference intensity + units: unspecified + rename_as: zero_air_ref_intensity + 0-Air Ref. Rel Intensity: + description: 0-air reference relative intensity + units: unspecified + rename_as: zero_air_ref_rel_intensity + 0-Air Ref. Intensity valid: + description: 0-air reference intensity validity + units: unspecified + rename_as: zero_air_ref_intensity_valid + MeasMode: + description: Measurement mode + units: unspecified + rename_as: measurement_mode + SampleSource: + description: Sample source + units: unspecified + rename_as: sample_source diff --git a/instruments/dictionaries/ICAD_NO2.yaml b/instruments/dictionaries/ICAD_NO2.yaml index 65cc46c..5e6e0db 100644 --- a/instruments/dictionaries/ICAD_NO2.yaml +++ b/instruments/dictionaries/ICAD_NO2.yaml @@ -1,113 +1,113 @@ -table_header: - Start Date/Time (UTC): - description: Start date and time of the measurement in UTC - units: YYYY-MM-DD HH:MM:SS - rename_as: start_datetime_utc - Duration (s): - description: Duration of the measurement in seconds - units: seconds - rename_as: duration_seconds - NO2 (ppb): - description: NO2 concentration - units: ppb - rename_as: no2_concentration_ppb - NO2 Uncertainty (ppb): - description: Uncertainty in NO2 concentration - units: ppb - rename_as: no2_uncertainty_ppb - H2O (ppb): - description: H2O concentration - units: ppb - rename_as: h2o_concentration_ppb - H2O Uncertainty (ppb): - description: Uncertainty in H2O concentration - units: ppb - rename_as: h2o_uncertainty_ppb - CHOCHO (ppb): - description: CHOCHO concentration - units: ppb - rename_as: chocho_concentration_ppb - CHOCHO Uncertainty (ppb): - description: Uncertainty in CHOCHO concentration - units: ppb - rename_as: chocho_uncertainty_ppb - File Number: - description: File number - units: unspecified - rename_as: file_number - Light Intensity: - description: Light intensity - units: unspecified - rename_as: light_intensity - 10_#ICEDOAS iter.: - description: Number of ICEDOAS iterations - units: unspecified - rename_as: icedoas_iterations - Cell Pressure: - description: Cell pressure - units: unspecified - rename_as: cell_pressure - Ambient Pressure: - description: Ambient pressure - units: unspecified - rename_as: ambient_pressure - Cell Temp: - description: Cell temperature - units: unspecified - rename_as: cell_temperature - Spec Temp: - description: Spectrometer temperature - units: unspecified - rename_as: spec_temperature - Lat: - description: Latitude - units: unspecified - rename_as: latitude - Lon: - description: Longitude - units: unspecified - rename_as: longitude - Height: - description: Height - units: unspecified - rename_as: height - Speed: - description: Speed - units: unspecified - rename_as: speed - GPSQuality: - description: GPS quality - units: unspecified - rename_as: gps_quality - 0-Air Ref. Time: - description: 0-air reference time - units: unspecified - rename_as: zero_air_ref_time - 0-Air Ref. Duration: - description: 0-air reference duration - units: unspecified - rename_as: zero_air_ref_duration - 0-Air Ref. File Number: - description: 0-air reference file number - units: unspecified - rename_as: zero_air_ref_file_number - 0-Air Ref. Intensity: - description: 0-air reference intensity - units: unspecified - rename_as: zero_air_ref_intensity - 0-Air Ref. Rel Intensity: - description: 0-air reference relative intensity - units: unspecified - rename_as: zero_air_ref_relative_intensity - 0-Air Ref. Intensity valid: - description: 0-air reference intensity validity - units: unspecified - rename_as: zero_air_ref_intensity_valid - MeasMode: - description: Measurement mode - units: unspecified - rename_as: measurement_mode - SampleSource: - description: Sample source - units: unspecified - rename_as: sample_source +table_header: + Start Date/Time (UTC): + description: Start date and time of the measurement in UTC + units: YYYY-MM-DD HH:MM:SS + rename_as: start_datetime_utc + Duration (s): + description: Duration of the measurement in seconds + units: seconds + rename_as: duration_seconds + NO2 (ppb): + description: NO2 concentration + units: ppb + rename_as: no2_concentration_ppb + NO2 Uncertainty (ppb): + description: Uncertainty in NO2 concentration + units: ppb + rename_as: no2_uncertainty_ppb + H2O (ppb): + description: H2O concentration + units: ppb + rename_as: h2o_concentration_ppb + H2O Uncertainty (ppb): + description: Uncertainty in H2O concentration + units: ppb + rename_as: h2o_uncertainty_ppb + CHOCHO (ppb): + description: CHOCHO concentration + units: ppb + rename_as: chocho_concentration_ppb + CHOCHO Uncertainty (ppb): + description: Uncertainty in CHOCHO concentration + units: ppb + rename_as: chocho_uncertainty_ppb + File Number: + description: File number + units: unspecified + rename_as: file_number + Light Intensity: + description: Light intensity + units: unspecified + rename_as: light_intensity + 10_#ICEDOAS iter.: + description: Number of ICEDOAS iterations + units: unspecified + rename_as: icedoas_iterations + Cell Pressure: + description: Cell pressure + units: unspecified + rename_as: cell_pressure + Ambient Pressure: + description: Ambient pressure + units: unspecified + rename_as: ambient_pressure + Cell Temp: + description: Cell temperature + units: unspecified + rename_as: cell_temperature + Spec Temp: + description: Spectrometer temperature + units: unspecified + rename_as: spec_temperature + Lat: + description: Latitude + units: unspecified + rename_as: latitude + Lon: + description: Longitude + units: unspecified + rename_as: longitude + Height: + description: Height + units: unspecified + rename_as: height + Speed: + description: Speed + units: unspecified + rename_as: speed + GPSQuality: + description: GPS quality + units: unspecified + rename_as: gps_quality + 0-Air Ref. Time: + description: 0-air reference time + units: unspecified + rename_as: zero_air_ref_time + 0-Air Ref. Duration: + description: 0-air reference duration + units: unspecified + rename_as: zero_air_ref_duration + 0-Air Ref. File Number: + description: 0-air reference file number + units: unspecified + rename_as: zero_air_ref_file_number + 0-Air Ref. Intensity: + description: 0-air reference intensity + units: unspecified + rename_as: zero_air_ref_intensity + 0-Air Ref. Rel Intensity: + description: 0-air reference relative intensity + units: unspecified + rename_as: zero_air_ref_relative_intensity + 0-Air Ref. Intensity valid: + description: 0-air reference intensity validity + units: unspecified + rename_as: zero_air_ref_intensity_valid + MeasMode: + description: Measurement mode + units: unspecified + rename_as: measurement_mode + SampleSource: + description: Sample source + units: unspecified + rename_as: sample_source diff --git a/instruments/dictionaries/Lopap.yaml b/instruments/dictionaries/Lopap.yaml index 2560e18..5648c48 100644 --- a/instruments/dictionaries/Lopap.yaml +++ b/instruments/dictionaries/Lopap.yaml @@ -1,121 +1,121 @@ -table_header: - Date: - description: Date of the measurement - units: YYYY-MM-DD - rename_as: date - Time: - description: Time of the measurement - units: HH:MM:SS - rename_as: time - Ch1: - description: Channel 1 measurement - units: unspecified - rename_as: ch1 - 3_490.1: - description: Measurement value at 490.1 nm wavelength - units: unspecified - rename_as: wavelength_490_1 - 4_500.2: - description: Measurement value at 500.2 nm wavelength - units: unspecified - rename_as: wavelength_500_2 - 5_510.0: - description: Measurement value at 510.0 nm wavelength - units: unspecified - rename_as: wavelength_510_0 - 6_520.0: - description: Measurement value at 520.0 nm wavelength - units: unspecified - rename_as: wavelength_520_0 - 7_530.1: - description: Measurement value at 530.1 nm wavelength - units: unspecified - rename_as: wavelength_530_1 - 8_540.0: - description: Measurement value at 540.0 nm wavelength - units: unspecified - rename_as: wavelength_540_0 - 9_550.7: - description: Measurement value at 550.7 nm wavelength - units: unspecified - rename_as: wavelength_550_7 - 10_603.2: - description: Measurement value at 603.2 nm wavelength - units: unspecified - rename_as: wavelength_603_2 - 11_700.3: - description: Measurement value at 700.3 nm wavelength - units: unspecified - rename_as: wavelength_700_3 - 12_800.0: - description: Measurement value at 800.0 nm wavelength - units: unspecified - rename_as: wavelength_800_0 - 13_Ch2: - description: Channel 2 measurement - units: unspecified - rename_as: ch2 - 14_500.5: - description: Measurement value at 500.5 nm wavelength - units: unspecified - rename_as: wavelength_500_5 - 15_510.3: - description: Measurement value at 510.3 nm wavelength - units: unspecified - rename_as: wavelength_510_3 - 16_520.5: - description: Measurement value at 520.5 nm wavelength - units: unspecified - rename_as: wavelength_520_5 - 17_530.7: - description: Measurement value at 530.7 nm wavelength - units: unspecified - rename_as: wavelength_530_7 - 18_540.8: - description: Measurement value at 540.8 nm wavelength - units: unspecified - rename_as: wavelength_540_8 - 19_550.5: - description: Measurement value at 550.5 nm wavelength - units: unspecified - rename_as: wavelength_550_5 - 20_550.8: - description: Measurement value at 550.8 nm wavelength - units: unspecified - rename_as: wavelength_550_8 - 21_560.9: - description: Measurement value at 560.9 nm wavelength - units: unspecified - rename_as: wavelength_560_9 - 22_570.9: - description: Measurement value at 570.9 nm wavelength - units: unspecified - rename_as: wavelength_570_9 - 23_581.2: - description: Measurement value at 581.2 nm wavelength - units: unspecified - rename_as: wavelength_581_2 - 24_586.2: - description: Measurement value at 586.2 nm wavelength - units: unspecified - rename_as: wavelength_586_2 - 25_591.2: - description: Measurement value at 591.2 nm wavelength - units: unspecified - rename_as: wavelength_591_2 - 26_596.1: - description: Measurement value at 596.1 nm wavelength - units: unspecified - rename_as: wavelength_596_1 - 27_601.1: - description: Measurement value at 601.1 nm wavelength - units: unspecified - rename_as: wavelength_601_1 - 28_606.4: - description: Measurement value at 606.4 nm wavelength - units: unspecified - rename_as: wavelength_606_4 - 29_611.3: - description: Measurement value at 611.3 nm wavelength - units: unspecified - rename_as: wavelength_611_3 +table_header: + Date: + description: Date of the measurement + units: YYYY-MM-DD + rename_as: date + Time: + description: Time of the measurement + units: HH:MM:SS + rename_as: time + Ch1: + description: Channel 1 measurement + units: unspecified + rename_as: ch1 + 3_490.1: + description: Measurement value at 490.1 nm wavelength + units: unspecified + rename_as: wavelength_490_1 + 4_500.2: + description: Measurement value at 500.2 nm wavelength + units: unspecified + rename_as: wavelength_500_2 + 5_510.0: + description: Measurement value at 510.0 nm wavelength + units: unspecified + rename_as: wavelength_510_0 + 6_520.0: + description: Measurement value at 520.0 nm wavelength + units: unspecified + rename_as: wavelength_520_0 + 7_530.1: + description: Measurement value at 530.1 nm wavelength + units: unspecified + rename_as: wavelength_530_1 + 8_540.0: + description: Measurement value at 540.0 nm wavelength + units: unspecified + rename_as: wavelength_540_0 + 9_550.7: + description: Measurement value at 550.7 nm wavelength + units: unspecified + rename_as: wavelength_550_7 + 10_603.2: + description: Measurement value at 603.2 nm wavelength + units: unspecified + rename_as: wavelength_603_2 + 11_700.3: + description: Measurement value at 700.3 nm wavelength + units: unspecified + rename_as: wavelength_700_3 + 12_800.0: + description: Measurement value at 800.0 nm wavelength + units: unspecified + rename_as: wavelength_800_0 + 13_Ch2: + description: Channel 2 measurement + units: unspecified + rename_as: ch2 + 14_500.5: + description: Measurement value at 500.5 nm wavelength + units: unspecified + rename_as: wavelength_500_5 + 15_510.3: + description: Measurement value at 510.3 nm wavelength + units: unspecified + rename_as: wavelength_510_3 + 16_520.5: + description: Measurement value at 520.5 nm wavelength + units: unspecified + rename_as: wavelength_520_5 + 17_530.7: + description: Measurement value at 530.7 nm wavelength + units: unspecified + rename_as: wavelength_530_7 + 18_540.8: + description: Measurement value at 540.8 nm wavelength + units: unspecified + rename_as: wavelength_540_8 + 19_550.5: + description: Measurement value at 550.5 nm wavelength + units: unspecified + rename_as: wavelength_550_5 + 20_550.8: + description: Measurement value at 550.8 nm wavelength + units: unspecified + rename_as: wavelength_550_8 + 21_560.9: + description: Measurement value at 560.9 nm wavelength + units: unspecified + rename_as: wavelength_560_9 + 22_570.9: + description: Measurement value at 570.9 nm wavelength + units: unspecified + rename_as: wavelength_570_9 + 23_581.2: + description: Measurement value at 581.2 nm wavelength + units: unspecified + rename_as: wavelength_581_2 + 24_586.2: + description: Measurement value at 586.2 nm wavelength + units: unspecified + rename_as: wavelength_586_2 + 25_591.2: + description: Measurement value at 591.2 nm wavelength + units: unspecified + rename_as: wavelength_591_2 + 26_596.1: + description: Measurement value at 596.1 nm wavelength + units: unspecified + rename_as: wavelength_596_1 + 27_601.1: + description: Measurement value at 601.1 nm wavelength + units: unspecified + rename_as: wavelength_601_1 + 28_606.4: + description: Measurement value at 606.4 nm wavelength + units: unspecified + rename_as: wavelength_606_4 + 29_611.3: + description: Measurement value at 611.3 nm wavelength + units: unspecified + rename_as: wavelength_611_3 diff --git a/instruments/dictionaries/Preassure.yaml b/instruments/dictionaries/Preassure.yaml index 6520f30..7a94ebf 100644 --- a/instruments/dictionaries/Preassure.yaml +++ b/instruments/dictionaries/Preassure.yaml @@ -1,81 +1,81 @@ -table_header: - Date: - description: Date of the measurement - units: YYYY-MM-DD - rename_as: date - Time: - description: Time of the measurement - units: HH:MM:SS - rename_as: time - Vapore-Pressure 1 in: - description: Vapor pressure measurement 1 - units: unspecified - rename_as: vapore_pressure_1_in - Vapore-Pressure 2 in: - description: Vapor pressure measurement 2 - units: unspecified - rename_as: vapore_pressure_2_in - Baratron 1 in: - description: Baratron measurement 1 - units: unspecified - rename_as: baratron_1_in - Baratron 2 in: - description: Baratron measurement 2 - units: unspecified - rename_as: baratron_2_in - Baratron 3 in: - description: Baratron measurement 3 - units: unspecified - rename_as: baratron_3_in - Baratron 4 in: - description: Baratron measurement 4 - units: unspecified - rename_as: baratron_4_in - Temp. Ice-Sample in: - description: Temperature of ice sample - units: Celcius - rename_as: temp_ice_sample_in - Temp. Heated-Sample in: - description: Temperature of heated sample - units: Celcius - rename_as: temp_heated_sample_in - Temp. Cooler 1 in: - description: Temperature of cooler 1 - units: Celcius - rename_as: temp_cooler_1_in - Temp. Cooler 2 in: - description: Temperature of cooler 2 - units: Celcius - rename_as: temp_cooler_2_in - Flow Gas 1 in: - description: Gas flow rate 1 - units: unspecified - rename_as: flow_gas_1_in - Pressure Chamber in: - description: Pressure in chamber - units: unspecified - rename_as: pressure_chamber_in - X in: - description: X coordinate - units: unspecified - rename_as: x_in - Y in: - description: Y coordinate - units: unspecified - rename_as: y_in - Z in: - description: Z coordinate - units: unspecified - rename_as: z_in - None in: - description: None measurement - units: unspecified - rename_as: none_in - Temp. Sealing in: - description: Temperature of sealing - units: Celcius - rename_as: temp_sealing_in - Flow Ice-Sample in: - description: Flow of ice sample - units: unspecified - rename_as: flow_ice_sample_in +table_header: + Date: + description: Date of the measurement + units: YYYY-MM-DD + rename_as: date + Time: + description: Time of the measurement + units: HH:MM:SS + rename_as: time + Vapore-Pressure 1 in: + description: Vapor pressure measurement 1 + units: unspecified + rename_as: vapore_pressure_1_in + Vapore-Pressure 2 in: + description: Vapor pressure measurement 2 + units: unspecified + rename_as: vapore_pressure_2_in + Baratron 1 in: + description: Baratron measurement 1 + units: unspecified + rename_as: baratron_1_in + Baratron 2 in: + description: Baratron measurement 2 + units: unspecified + rename_as: baratron_2_in + Baratron 3 in: + description: Baratron measurement 3 + units: unspecified + rename_as: baratron_3_in + Baratron 4 in: + description: Baratron measurement 4 + units: unspecified + rename_as: baratron_4_in + Temp. Ice-Sample in: + description: Temperature of ice sample + units: Celcius + rename_as: temp_ice_sample_in + Temp. Heated-Sample in: + description: Temperature of heated sample + units: Celcius + rename_as: temp_heated_sample_in + Temp. Cooler 1 in: + description: Temperature of cooler 1 + units: Celcius + rename_as: temp_cooler_1_in + Temp. Cooler 2 in: + description: Temperature of cooler 2 + units: Celcius + rename_as: temp_cooler_2_in + Flow Gas 1 in: + description: Gas flow rate 1 + units: unspecified + rename_as: flow_gas_1_in + Pressure Chamber in: + description: Pressure in chamber + units: unspecified + rename_as: pressure_chamber_in + X in: + description: X coordinate + units: unspecified + rename_as: x_in + Y in: + description: Y coordinate + units: unspecified + rename_as: y_in + Z in: + description: Z coordinate + units: unspecified + rename_as: z_in + None in: + description: None measurement + units: unspecified + rename_as: none_in + Temp. Sealing in: + description: Temperature of sealing + units: Celcius + rename_as: temp_sealing_in + Flow Ice-Sample in: + description: Flow of ice sample + units: unspecified + rename_as: flow_ice_sample_in diff --git a/instruments/dictionaries/RGA.yaml b/instruments/dictionaries/RGA.yaml index 04ad950..d0cd09d 100644 --- a/instruments/dictionaries/RGA.yaml +++ b/instruments/dictionaries/RGA.yaml @@ -1,37 +1,37 @@ -table_header: - Time(s): - description: Time elapsed since start - units: seconds - rename_as: time_seconds - Channel#1: - description: Measurement from Channel 1 - units: unspecified - rename_as: channel_1_measurement - Channel#2: - description: Measurement from Channel 2 - units: unspecified - rename_as: channel_2_measurement - Channel#3: - description: Measurement from Channel 3 - units: unspecified - rename_as: channel_3_measurement - Channel#4: - description: Measurement from Channel 4 - units: unspecified - rename_as: channel_4_measurement - Channel#5: - description: Measurement from Channel 5 - units: unspecified - rename_as: channel_5_measurement - Channel#6: - description: Measurement from Channel 6 - units: unspecified - rename_as: channel_6_measurement - Channel#7: - description: Measurement from Channel 7 - units: unspecified - rename_as: channel_7_measurement - Channel#8: - description: Measurement from Channel 8 - units: unspecified - rename_as: channel_8_measurement +table_header: + Time(s): + description: Time elapsed since start + units: seconds + rename_as: time_seconds + Channel#1: + description: Measurement from Channel 1 + units: unspecified + rename_as: channel_1_measurement + Channel#2: + description: Measurement from Channel 2 + units: unspecified + rename_as: channel_2_measurement + Channel#3: + description: Measurement from Channel 3 + units: unspecified + rename_as: channel_3_measurement + Channel#4: + description: Measurement from Channel 4 + units: unspecified + rename_as: channel_4_measurement + Channel#5: + description: Measurement from Channel 5 + units: unspecified + rename_as: channel_5_measurement + Channel#6: + description: Measurement from Channel 6 + units: unspecified + rename_as: channel_6_measurement + Channel#7: + description: Measurement from Channel 7 + units: unspecified + rename_as: channel_7_measurement + Channel#8: + description: Measurement from Channel 8 + units: unspecified + rename_as: channel_8_measurement diff --git a/instruments/dictionaries/T200_NOx.yaml b/instruments/dictionaries/T200_NOx.yaml index 547ff97..4fa3451 100644 --- a/instruments/dictionaries/T200_NOx.yaml +++ b/instruments/dictionaries/T200_NOx.yaml @@ -1,21 +1,21 @@ -table_header: - Date: - description: Date of the measurement - units: YYYY-MM-DD - rename_as: date - Time: - description: Time of the measurement - units: HH:MM:SS - rename_as: time - NO: - description: NO concentration - units: ppb, ppm - rename_as: no_concentration - NO2: - description: NO2 concentration - units: ppb, ppm - rename_as: no2_concentration - NOx: - description: NOx concentration - units: ppb, ppm - rename_as: nox_concentration +table_header: + Date: + description: Date of the measurement + units: YYYY-MM-DD + rename_as: date + Time: + description: Time of the measurement + units: HH:MM:SS + rename_as: time + NO: + description: NO concentration + units: ppb, ppm + rename_as: no_concentration + NO2: + description: NO2 concentration + units: ppb, ppm + rename_as: no2_concentration + NOx: + description: NOx concentration + units: ppb, ppm + rename_as: nox_concentration diff --git a/instruments/dictionaries/T360U_CO2.yaml b/instruments/dictionaries/T360U_CO2.yaml index 8778a1a..b064d4d 100644 --- a/instruments/dictionaries/T360U_CO2.yaml +++ b/instruments/dictionaries/T360U_CO2.yaml @@ -1,13 +1,13 @@ -table_header: - Date: - description: Date of the measurement - units: YYYY-MM-DD - rename_as: date - Time: - description: Time of the measurement - units: HH:MM:SS - rename_as: time - CO2: - description: CO2 concentration - units: ppm - rename_as: co2_concentration +table_header: + Date: + description: Date of the measurement + units: YYYY-MM-DD + rename_as: date + Time: + description: Time of the measurement + units: HH:MM:SS + rename_as: time + CO2: + description: CO2 concentration + units: ppm + rename_as: co2_concentration diff --git a/instruments/dictionaries/gas.yaml b/instruments/dictionaries/gas.yaml index 6382d06..6499151 100644 --- a/instruments/dictionaries/gas.yaml +++ b/instruments/dictionaries/gas.yaml @@ -1,102 +1,102 @@ -table_header: - Date_Time: - description: Date and time of the measurement - units: YYYY-MM-DD HH:MM:SS - rename_as: date_time - HoribaNO: - description: Horiba NO concentration - units: unspecified - rename_as: horiba_no - HoribaNOy: - description: Horiba NOy concentration - units: unspecified - rename_as: horiba_noy - Thermo42C_NO: - description: Thermo 42C NO concentration - units: unspecified - rename_as: thermo42c_no - Thermo42C_NOx: - description: Thermo 42C NOx concentration - units: unspecified - rename_as: thermo42c_nox - APHA370_CH4: - description: APHA370 CH4 concentration - units: unspecified - rename_as: apha370_ch4 - APHA370_THC: - description: APHA370 THC concentration - units: unspecified - rename_as: apha370_thc - HygroclipRH: - description: Hygroclip relative humidity - units: percentage - rename_as: hygroclip_rh - HygroclipT: - description: Hygroclip temperature - units: Celsius - rename_as: hygroclip_temp - ML9850SO2: - description: ML9850 SO2 concentration - units: unspecified - rename_as: ml9850_so2 - Ozone49C: - description: Ozone 49C concentration - units: unspecified - rename_as: ozone_49c - PAMrh: - description: PAM relative humidity - units: percentage - rename_as: pam_rh - PAMt: - description: PAM temperature - units: Celsius - rename_as: pam_temp - xxxal: - description: Measurement (xxxal) - units: unspecified - rename_as: measurement_xxxal - ThermoCouple0: - description: Thermo couple measurement 0 - units: unspecified - rename_as: thermocouple_0 - ThermoCouple1: - description: Thermo couple measurement 1 - units: unspecified - rename_as: thermocouple_1 - ThermoCouple2: - description: Thermo couple measurement 2 - units: unspecified - rename_as: thermocouple_2 - ThermoCouple3: - description: Thermo couple measurement 3 - units: unspecified - rename_as: thermocouple_3 - xxxTC: - description: Measurement (xxxTC) - units: unspecified - rename_as: measurement_xxxtc - CPC: - description: CPC concentration - units: unspecified - rename_as: cpc_concentration - xxx: - description: Measurement (xxx) - units: unspecified - rename_as: measurement_xxx - LicorH2Odelta: - description: Licor H2O delta - units: unspecified - rename_as: licor_h2o_delta - LicorCO2delta: - description: Licor CO2 delta - units: unspecified - rename_as: licor_co2_delta - 2BO2: - description: 2BO2 concentration - units: unspecified - rename_as: 2bo2_concentration - HoribaCO: - description: Horiba CO concentration - units: unspecified - rename_as: horiba_co_concentration - +table_header: + Date_Time: + description: Date and time of the measurement + units: YYYY-MM-DD HH:MM:SS + rename_as: date_time + HoribaNO: + description: Horiba NO concentration + units: unspecified + rename_as: horiba_no + HoribaNOy: + description: Horiba NOy concentration + units: unspecified + rename_as: horiba_noy + Thermo42C_NO: + description: Thermo 42C NO concentration + units: unspecified + rename_as: thermo42c_no + Thermo42C_NOx: + description: Thermo 42C NOx concentration + units: unspecified + rename_as: thermo42c_nox + APHA370_CH4: + description: APHA370 CH4 concentration + units: unspecified + rename_as: apha370_ch4 + APHA370_THC: + description: APHA370 THC concentration + units: unspecified + rename_as: apha370_thc + HygroclipRH: + description: Hygroclip relative humidity + units: percentage + rename_as: hygroclip_rh + HygroclipT: + description: Hygroclip temperature + units: Celsius + rename_as: hygroclip_temp + ML9850SO2: + description: ML9850 SO2 concentration + units: unspecified + rename_as: ml9850_so2 + Ozone49C: + description: Ozone 49C concentration + units: unspecified + rename_as: ozone_49c + PAMrh: + description: PAM relative humidity + units: percentage + rename_as: pam_rh + PAMt: + description: PAM temperature + units: Celsius + rename_as: pam_temp + xxxal: + description: Measurement (xxxal) + units: unspecified + rename_as: measurement_xxxal + ThermoCouple0: + description: Thermo couple measurement 0 + units: unspecified + rename_as: thermocouple_0 + ThermoCouple1: + description: Thermo couple measurement 1 + units: unspecified + rename_as: thermocouple_1 + ThermoCouple2: + description: Thermo couple measurement 2 + units: unspecified + rename_as: thermocouple_2 + ThermoCouple3: + description: Thermo couple measurement 3 + units: unspecified + rename_as: thermocouple_3 + xxxTC: + description: Measurement (xxxTC) + units: unspecified + rename_as: measurement_xxxtc + CPC: + description: CPC concentration + units: unspecified + rename_as: cpc_concentration + xxx: + description: Measurement (xxx) + units: unspecified + rename_as: measurement_xxx + LicorH2Odelta: + description: Licor H2O delta + units: unspecified + rename_as: licor_h2o_delta + LicorCO2delta: + description: Licor CO2 delta + units: unspecified + rename_as: licor_co2_delta + 2BO2: + description: 2BO2 concentration + units: unspecified + rename_as: 2bo2_concentration + HoribaCO: + description: Horiba CO concentration + units: unspecified + rename_as: horiba_co_concentration + diff --git a/instruments/dictionaries/smps.yaml b/instruments/dictionaries/smps.yaml index 20ad712..0f89d16 100644 --- a/instruments/dictionaries/smps.yaml +++ b/instruments/dictionaries/smps.yaml @@ -1,113 +1,113 @@ -table_header: - #0_Sample #: - # description: Sample number - # units: unspecified - # rename_as: 0_sample_number - Date: - description: Date of the measurement - units: YYYY-MM-DD - rename_as: date - Start Time: - description: Start time of the measurement - units: HH:MM:SS - rename_as: start_time - Sample Temp (C): - description: Sample temperature - units: Celsius - rename_as: sample_temperature_celsius - Sample Pressure (kPa): - description: Sample pressure - units: kilopascal - rename_as: sample_pressure_kPa - Relative Humidity (%): - description: Relative humidity - units: percentage - rename_as: relative_humidity_percentage - Mean Free Path (m): - description: Mean free path - units: meter - rename_as: mean_free_path_meters - Gas Viscosity (Pa*s): - description: Gas viscosity - units: Pascal-second - rename_as: gas_viscosity_Pa_s - Diameter Midpoint (nm): - description: Diameter midpoint - units: nanometer - rename_as: diameter_midpoint_nm - to_135_Diameter (nm): - description: Particle diameter measurements from 10 nm to 135 nm - units: nanometer - rename_as: to_135_diameter_nm - to_290_Diameter (nm): - description: Particle diameter measurements from 136 nm to 290 nm - units: nanometer - rename_as: to_290_diameter_nm - to_500_Diameter (nm): - description: Particle diameter measurements from 291 nm to 500 nm - units: nanometer - rename_as: to_500_diameter_nm - Scan Time (s): - description: Scan time - units: seconds - rename_as: scan_time_seconds - Retrace Time (s): - description: Retrace time - units: seconds - rename_as: retrace_time_seconds - Scan Resolution (Hz): - description: Scan resolution - units: Hertz - rename_as: scan_resolution_Hz - Scans Per Sample: - description: Number of scans per sample - units: unspecified - rename_as: scans_per_sample - Sheath Flow (L/min): - description: Sheath flow rate - units: liters per minute - rename_as: sheath_flow_rate_L_per_min - Aerosol Flow (L/min): - description: Aerosol flow rate - units: liters per minute - rename_as: aerosol_flow_rate_L_per_min - Bypass Flow (L/min): - description: Bypass flow rate - units: liters per minute - rename_as: bypass_flow_rate_L_per_min - Low Voltage (V): - description: Low voltage - units: volts - rename_as: low_voltage_V - High Voltage (V): - description: High voltage - units: volts - rename_as: high_voltage_V - Lower Size (nm): - description: Lower size limit - units: nanometer - rename_as: lower_size_limit_nm - Upper Size (nm): - description: Upper size limit - units: nanometer - rename_as: upper_size_limit_nm - Density (g/cm³): - description: Particle density - units: grams per cubic centimeter - rename_as: particle_density_g_per_cm3 - td + 0.5 (s): - description: Time delay plus 0.5 seconds - units: seconds - rename_as: td_plus_0.5_seconds - tf (s): - description: Final time - units: seconds - rename_as: tf_seconds - D50 (nm): - description: Median particle diameter - units: nanometer - rename_as: d50_nm - Neutralizer: - description: Type of neutralizer - units: unspecified - rename_as: neutralizer_type +table_header: + #0_Sample #: + # description: Sample number + # units: unspecified + # rename_as: 0_sample_number + Date: + description: Date of the measurement + units: YYYY-MM-DD + rename_as: date + Start Time: + description: Start time of the measurement + units: HH:MM:SS + rename_as: start_time + Sample Temp (C): + description: Sample temperature + units: Celsius + rename_as: sample_temperature_celsius + Sample Pressure (kPa): + description: Sample pressure + units: kilopascal + rename_as: sample_pressure_kPa + Relative Humidity (%): + description: Relative humidity + units: percentage + rename_as: relative_humidity_percentage + Mean Free Path (m): + description: Mean free path + units: meter + rename_as: mean_free_path_meters + Gas Viscosity (Pa*s): + description: Gas viscosity + units: Pascal-second + rename_as: gas_viscosity_Pa_s + Diameter Midpoint (nm): + description: Diameter midpoint + units: nanometer + rename_as: diameter_midpoint_nm + to_135_Diameter (nm): + description: Particle diameter measurements from 10 nm to 135 nm + units: nanometer + rename_as: to_135_diameter_nm + to_290_Diameter (nm): + description: Particle diameter measurements from 136 nm to 290 nm + units: nanometer + rename_as: to_290_diameter_nm + to_500_Diameter (nm): + description: Particle diameter measurements from 291 nm to 500 nm + units: nanometer + rename_as: to_500_diameter_nm + Scan Time (s): + description: Scan time + units: seconds + rename_as: scan_time_seconds + Retrace Time (s): + description: Retrace time + units: seconds + rename_as: retrace_time_seconds + Scan Resolution (Hz): + description: Scan resolution + units: Hertz + rename_as: scan_resolution_Hz + Scans Per Sample: + description: Number of scans per sample + units: unspecified + rename_as: scans_per_sample + Sheath Flow (L/min): + description: Sheath flow rate + units: liters per minute + rename_as: sheath_flow_rate_L_per_min + Aerosol Flow (L/min): + description: Aerosol flow rate + units: liters per minute + rename_as: aerosol_flow_rate_L_per_min + Bypass Flow (L/min): + description: Bypass flow rate + units: liters per minute + rename_as: bypass_flow_rate_L_per_min + Low Voltage (V): + description: Low voltage + units: volts + rename_as: low_voltage_V + High Voltage (V): + description: High voltage + units: volts + rename_as: high_voltage_V + Lower Size (nm): + description: Lower size limit + units: nanometer + rename_as: lower_size_limit_nm + Upper Size (nm): + description: Upper size limit + units: nanometer + rename_as: upper_size_limit_nm + Density (g/cm³): + description: Particle density + units: grams per cubic centimeter + rename_as: particle_density_g_per_cm3 + td + 0.5 (s): + description: Time delay plus 0.5 seconds + units: seconds + rename_as: td_plus_0.5_seconds + tf (s): + description: Final time + units: seconds + rename_as: tf_seconds + D50 (nm): + description: Median particle diameter + units: nanometer + rename_as: d50_nm + Neutralizer: + description: Type of neutralizer + units: unspecified + rename_as: neutralizer_type diff --git a/instruments/readers/acsm_tofware_reader.py b/instruments/readers/acsm_tofware_reader.py index b56019b..a519c18 100644 --- a/instruments/readers/acsm_tofware_reader.py +++ b/instruments/readers/acsm_tofware_reader.py @@ -1,223 +1,223 @@ -import sys -import os -import pandas as pd -import collections -import yaml - -#root_dir = os.path.abspath(os.curdir) -#sys.path.append(root_dir) -import utils.g5505_utils as utils - - - - -def read_acsm_files_as_dict(filename: str, instruments_dir: str = None, work_with_copy: bool = True): - # If instruments_dir is not provided, use the default path relative to the module directory - if not instruments_dir: - # Assuming the instruments folder is one level up from the source module directory - module_dir = os.path.dirname(__file__) - instruments_dir = os.path.join(module_dir, '..') - - # Normalize the path (resolves any '..' in the path) - instrument_configs_path = os.path.abspath(os.path.join(instruments_dir,'dictionaries','ACSM_TOFWARE.yaml')) - - with open(instrument_configs_path,'r') as stream: - try: - config_dict = yaml.load(stream, Loader=yaml.FullLoader) - except yaml.YAMLError as exc: - print(exc) - # Verify if file can be read by available intrument configurations. - #if not any(key in filename.replace(os.sep,'/') for key in config_dict.keys()): - # return {} - - - - #TODO: this may be prone to error if assumed folder structure is non compliant - - - description_dict = config_dict.get('table_header',{}) - - file_encoding = config_dict['config_text_reader'].get('file_encoding','utf-8') - separator = config_dict['config_text_reader'].get('separator',None) - table_header = config_dict['config_text_reader'].get('table_header',None) - timestamp_variables = config_dict['config_text_reader'].get('timestamp',[]) - datetime_format = config_dict['config_text_reader'].get('datetime_format',[]) - - - - # Read header as a dictionary and detect where data table starts - header_dict = {} - data_start = False - # Work with copy of the file for safety - if work_with_copy: - tmp_filename = utils.make_file_copy(source_file_path=filename) - else: - tmp_filename = filename - - if not isinstance(table_header, list): - table_header = [table_header] - file_encoding = [file_encoding] - separator = [separator] - - with open(tmp_filename,'rb') as f: - table_preamble = [] - for line_number, line in enumerate(f): - - - for tb_idx, tb in enumerate(table_header): - if tb in line.decode(file_encoding[tb_idx]): - break - - if tb in line.decode(file_encoding[tb_idx]): - list_of_substrings = line.decode(file_encoding[tb_idx]).split(separator[tb_idx].replace('\\t','\t')) - - # Count occurrences of each substring - substring_counts = collections.Counter(list_of_substrings) - data_start = True - # Generate column names with appended index only for repeated substrings - column_names = [f"{i}_{name.strip()}" if substring_counts[name] > 1 else name.strip() for i, name in enumerate(list_of_substrings)] - - #column_names = [str(i)+'_'+name.strip() for i, name in enumerate(list_of_substrings)] - #column_names = [] - #for i, name in enumerate(list_of_substrings): - # column_names.append(str(i)+'_'+name) - - #print(line_number, len(column_names ),'\n') - break - # Subdivide line into words, and join them by single space. - # I asumme this can produce a cleaner line that contains no weird separator characters \t \r or extra spaces and so on. - list_of_substrings = line.decode(file_encoding[tb_idx]).split() - # TODO: ideally we should use a multilinear string but the yalm parser is not recognizing \n as special character - #line = ' '.join(list_of_substrings+['\n']) - #line = ' '.join(list_of_substrings) - table_preamble.append(' '.join([item for item in list_of_substrings]))# += new_line - - - # TODO: it does not work with separator as none :(. fix for RGA - try: - df = pd.read_csv(tmp_filename, - delimiter = separator[tb_idx].replace('\\t','\t'), - header=line_number, - #encoding='latin-1', - encoding = file_encoding[tb_idx], - names=column_names, - skip_blank_lines=True) - - df_numerical_attrs = df.select_dtypes(include ='number') - df_categorical_attrs = df.select_dtypes(exclude='number') - numerical_variables = [item for item in df_numerical_attrs.columns] - - # Consolidate into single timestamp column the separate columns 'date' 'time' specified in text_data_source.yaml - if timestamp_variables: - #df_categorical_attrs['timestamps'] = [' '.join(df_categorical_attrs.loc[i,timestamp_variables].to_numpy()) for i in df.index] - #df_categorical_attrs['timestamps'] = [ df_categorical_attrs.loc[i,'0_Date']+' '+df_categorical_attrs.loc[i,'1_Time'] for i in df.index] - - - #df_categorical_attrs['timestamps'] = df_categorical_attrs[timestamp_variables].astype(str).agg(' '.join, axis=1) - timestamps_name = ' '.join(timestamp_variables) - df_categorical_attrs[ timestamps_name] = df_categorical_attrs[timestamp_variables].astype(str).agg(' '.join, axis=1) - - valid_indices = [] - if datetime_format: - df_categorical_attrs[ timestamps_name] = pd.to_datetime(df_categorical_attrs[ timestamps_name],format=datetime_format,errors='coerce') - valid_indices = df_categorical_attrs.dropna(subset=[timestamps_name]).index - df_categorical_attrs = df_categorical_attrs.loc[valid_indices,:] - df_numerical_attrs = df_numerical_attrs.loc[valid_indices,:] - - df_categorical_attrs[timestamps_name] = df_categorical_attrs[timestamps_name].dt.strftime(config_dict['default']['desired_format']) - startdate = df_categorical_attrs[timestamps_name].min() - enddate = df_categorical_attrs[timestamps_name].max() - - df_categorical_attrs[timestamps_name] = df_categorical_attrs[timestamps_name].astype(str) - #header_dict.update({'stastrrtdate':startdate,'enddate':enddate}) - header_dict['startdate']= str(startdate) - header_dict['enddate']=str(enddate) - - if len(timestamp_variables) > 1: - df_categorical_attrs = df_categorical_attrs.drop(columns = timestamp_variables) - - - #df_categorical_attrs.reindex(drop=True) - #df_numerical_attrs.reindex(drop=True) - - - - categorical_variables = [item for item in df_categorical_attrs.columns] - #### - #elif 'RGA' in filename: - # df_categorical_attrs = df_categorical_attrs.rename(columns={'0_Time(s)' : 'timestamps'}) - - ### - file_dict = {} - path_tail, path_head = os.path.split(tmp_filename) - - file_dict['name'] = path_head - # TODO: review this header dictionary, it may not be the best way to represent header data - file_dict['attributes_dict'] = header_dict - file_dict['datasets'] = [] - #### - - df = pd.concat((df_categorical_attrs,df_numerical_attrs),axis=1) - - #if numerical_variables: - dataset = {} - dataset['name'] = 'data_table'#_numerical_variables' - dataset['data'] = utils.convert_dataframe_to_np_structured_array(df) #df_numerical_attrs.to_numpy() - dataset['shape'] = dataset['data'].shape - dataset['dtype'] = type(dataset['data']) - #dataset['data_units'] = file_obj['wave']['data_units'] - # - # Create attribute descriptions based on description_dict - dataset['attributes'] = {} - - # Annotate column headers if description_dict is non empty - if description_dict: - for column_name in df.columns: - column_attr_dict = description_dict.get(column_name, - {'note':'there was no description available. Review instrument files.'}) - dataset['attributes'].update({column_name: utils.convert_attrdict_to_np_structured_array(column_attr_dict)}) - - #try: - # dataset['attributes'] = description_dict['table_header'].copy() - # for key in description_dict['table_header'].keys(): - # if not key in numerical_variables: - # dataset['attributes'].pop(key) # delete key - # else: - # dataset['attributes'][key] = utils.parse_attribute(dataset['attributes'][key]) - # if timestamps_name in categorical_variables: - # dataset['attributes'][timestamps_name] = utils.parse_attribute({'unit':'YYYY-MM-DD HH:MM:SS.ffffff'}) - #except ValueError as err: - # print(err) - - # Represent string values as fixed length strings in the HDF5 file, which need - # to be decoded as string when we read them. It provides better control than variable strings, - # at the expense of flexibility. - # https://docs.h5py.org/en/stable/strings.html - - - if table_preamble: - #header_dict["table_preamble"] = utils.convert_string_to_bytes(table_preamble) - tp_dataset = {} - tp_dataset['name'] = "table_preamble" - tp_dataset['data'] = utils.convert_string_to_bytes(table_preamble) - tp_dataset['shape'] = tp_dataset['data'].shape - tp_dataset['dtype'] = type(tp_dataset['data']) - tp_dataset['attributes'] = {} - file_dict['datasets'].append(tp_dataset) - - file_dict['datasets'].append(dataset) - - - #if categorical_variables: - # dataset = {} - # dataset['name'] = 'table_categorical_variables' - # dataset['data'] = dataframe_to_np_structured_array(df_categorical_attrs) #df_categorical_attrs.loc[:,categorical_variables].to_numpy() - # dataset['shape'] = dataset['data'].shape - # dataset['dtype'] = type(dataset['data']) - # if timestamps_name in categorical_variables: - # dataset['attributes'] = {timestamps_name: utils.parse_attribute({'unit':'YYYY-MM-DD HH:MM:SS.ffffff'})} - # file_dict['datasets'].append(dataset) - except: - return {} - +import sys +import os +import pandas as pd +import collections +import yaml + +#root_dir = os.path.abspath(os.curdir) +#sys.path.append(root_dir) +import utils.g5505_utils as utils + + + + +def read_acsm_files_as_dict(filename: str, instruments_dir: str = None, work_with_copy: bool = True): + # If instruments_dir is not provided, use the default path relative to the module directory + if not instruments_dir: + # Assuming the instruments folder is one level up from the source module directory + module_dir = os.path.dirname(__file__) + instruments_dir = os.path.join(module_dir, '..') + + # Normalize the path (resolves any '..' in the path) + instrument_configs_path = os.path.abspath(os.path.join(instruments_dir,'dictionaries','ACSM_TOFWARE.yaml')) + + with open(instrument_configs_path,'r') as stream: + try: + config_dict = yaml.load(stream, Loader=yaml.FullLoader) + except yaml.YAMLError as exc: + print(exc) + # Verify if file can be read by available intrument configurations. + #if not any(key in filename.replace(os.sep,'/') for key in config_dict.keys()): + # return {} + + + + #TODO: this may be prone to error if assumed folder structure is non compliant + + + description_dict = config_dict.get('table_header',{}) + + file_encoding = config_dict['config_text_reader'].get('file_encoding','utf-8') + separator = config_dict['config_text_reader'].get('separator',None) + table_header = config_dict['config_text_reader'].get('table_header',None) + timestamp_variables = config_dict['config_text_reader'].get('timestamp',[]) + datetime_format = config_dict['config_text_reader'].get('datetime_format',[]) + + + + # Read header as a dictionary and detect where data table starts + header_dict = {} + data_start = False + # Work with copy of the file for safety + if work_with_copy: + tmp_filename = utils.make_file_copy(source_file_path=filename) + else: + tmp_filename = filename + + if not isinstance(table_header, list): + table_header = [table_header] + file_encoding = [file_encoding] + separator = [separator] + + with open(tmp_filename,'rb') as f: + table_preamble = [] + for line_number, line in enumerate(f): + + + for tb_idx, tb in enumerate(table_header): + if tb in line.decode(file_encoding[tb_idx]): + break + + if tb in line.decode(file_encoding[tb_idx]): + list_of_substrings = line.decode(file_encoding[tb_idx]).split(separator[tb_idx].replace('\\t','\t')) + + # Count occurrences of each substring + substring_counts = collections.Counter(list_of_substrings) + data_start = True + # Generate column names with appended index only for repeated substrings + column_names = [f"{i}_{name.strip()}" if substring_counts[name] > 1 else name.strip() for i, name in enumerate(list_of_substrings)] + + #column_names = [str(i)+'_'+name.strip() for i, name in enumerate(list_of_substrings)] + #column_names = [] + #for i, name in enumerate(list_of_substrings): + # column_names.append(str(i)+'_'+name) + + #print(line_number, len(column_names ),'\n') + break + # Subdivide line into words, and join them by single space. + # I asumme this can produce a cleaner line that contains no weird separator characters \t \r or extra spaces and so on. + list_of_substrings = line.decode(file_encoding[tb_idx]).split() + # TODO: ideally we should use a multilinear string but the yalm parser is not recognizing \n as special character + #line = ' '.join(list_of_substrings+['\n']) + #line = ' '.join(list_of_substrings) + table_preamble.append(' '.join([item for item in list_of_substrings]))# += new_line + + + # TODO: it does not work with separator as none :(. fix for RGA + try: + df = pd.read_csv(tmp_filename, + delimiter = separator[tb_idx].replace('\\t','\t'), + header=line_number, + #encoding='latin-1', + encoding = file_encoding[tb_idx], + names=column_names, + skip_blank_lines=True) + + df_numerical_attrs = df.select_dtypes(include ='number') + df_categorical_attrs = df.select_dtypes(exclude='number') + numerical_variables = [item for item in df_numerical_attrs.columns] + + # Consolidate into single timestamp column the separate columns 'date' 'time' specified in text_data_source.yaml + if timestamp_variables: + #df_categorical_attrs['timestamps'] = [' '.join(df_categorical_attrs.loc[i,timestamp_variables].to_numpy()) for i in df.index] + #df_categorical_attrs['timestamps'] = [ df_categorical_attrs.loc[i,'0_Date']+' '+df_categorical_attrs.loc[i,'1_Time'] for i in df.index] + + + #df_categorical_attrs['timestamps'] = df_categorical_attrs[timestamp_variables].astype(str).agg(' '.join, axis=1) + timestamps_name = ' '.join(timestamp_variables) + df_categorical_attrs[ timestamps_name] = df_categorical_attrs[timestamp_variables].astype(str).agg(' '.join, axis=1) + + valid_indices = [] + if datetime_format: + df_categorical_attrs[ timestamps_name] = pd.to_datetime(df_categorical_attrs[ timestamps_name],format=datetime_format,errors='coerce') + valid_indices = df_categorical_attrs.dropna(subset=[timestamps_name]).index + df_categorical_attrs = df_categorical_attrs.loc[valid_indices,:] + df_numerical_attrs = df_numerical_attrs.loc[valid_indices,:] + + df_categorical_attrs[timestamps_name] = df_categorical_attrs[timestamps_name].dt.strftime(config_dict['default']['desired_format']) + startdate = df_categorical_attrs[timestamps_name].min() + enddate = df_categorical_attrs[timestamps_name].max() + + df_categorical_attrs[timestamps_name] = df_categorical_attrs[timestamps_name].astype(str) + #header_dict.update({'stastrrtdate':startdate,'enddate':enddate}) + header_dict['startdate']= str(startdate) + header_dict['enddate']=str(enddate) + + if len(timestamp_variables) > 1: + df_categorical_attrs = df_categorical_attrs.drop(columns = timestamp_variables) + + + #df_categorical_attrs.reindex(drop=True) + #df_numerical_attrs.reindex(drop=True) + + + + categorical_variables = [item for item in df_categorical_attrs.columns] + #### + #elif 'RGA' in filename: + # df_categorical_attrs = df_categorical_attrs.rename(columns={'0_Time(s)' : 'timestamps'}) + + ### + file_dict = {} + path_tail, path_head = os.path.split(tmp_filename) + + file_dict['name'] = path_head + # TODO: review this header dictionary, it may not be the best way to represent header data + file_dict['attributes_dict'] = header_dict + file_dict['datasets'] = [] + #### + + df = pd.concat((df_categorical_attrs,df_numerical_attrs),axis=1) + + #if numerical_variables: + dataset = {} + dataset['name'] = 'data_table'#_numerical_variables' + dataset['data'] = utils.convert_dataframe_to_np_structured_array(df) #df_numerical_attrs.to_numpy() + dataset['shape'] = dataset['data'].shape + dataset['dtype'] = type(dataset['data']) + #dataset['data_units'] = file_obj['wave']['data_units'] + # + # Create attribute descriptions based on description_dict + dataset['attributes'] = {} + + # Annotate column headers if description_dict is non empty + if description_dict: + for column_name in df.columns: + column_attr_dict = description_dict.get(column_name, + {'note':'there was no description available. Review instrument files.'}) + dataset['attributes'].update({column_name: utils.convert_attrdict_to_np_structured_array(column_attr_dict)}) + + #try: + # dataset['attributes'] = description_dict['table_header'].copy() + # for key in description_dict['table_header'].keys(): + # if not key in numerical_variables: + # dataset['attributes'].pop(key) # delete key + # else: + # dataset['attributes'][key] = utils.parse_attribute(dataset['attributes'][key]) + # if timestamps_name in categorical_variables: + # dataset['attributes'][timestamps_name] = utils.parse_attribute({'unit':'YYYY-MM-DD HH:MM:SS.ffffff'}) + #except ValueError as err: + # print(err) + + # Represent string values as fixed length strings in the HDF5 file, which need + # to be decoded as string when we read them. It provides better control than variable strings, + # at the expense of flexibility. + # https://docs.h5py.org/en/stable/strings.html + + + if table_preamble: + #header_dict["table_preamble"] = utils.convert_string_to_bytes(table_preamble) + tp_dataset = {} + tp_dataset['name'] = "table_preamble" + tp_dataset['data'] = utils.convert_string_to_bytes(table_preamble) + tp_dataset['shape'] = tp_dataset['data'].shape + tp_dataset['dtype'] = type(tp_dataset['data']) + tp_dataset['attributes'] = {} + file_dict['datasets'].append(tp_dataset) + + file_dict['datasets'].append(dataset) + + + #if categorical_variables: + # dataset = {} + # dataset['name'] = 'table_categorical_variables' + # dataset['data'] = dataframe_to_np_structured_array(df_categorical_attrs) #df_categorical_attrs.loc[:,categorical_variables].to_numpy() + # dataset['shape'] = dataset['data'].shape + # dataset['dtype'] = type(dataset['data']) + # if timestamps_name in categorical_variables: + # dataset['attributes'] = {timestamps_name: utils.parse_attribute({'unit':'YYYY-MM-DD HH:MM:SS.ffffff'})} + # file_dict['datasets'].append(dataset) + except: + return {} + return file_dict \ No newline at end of file diff --git a/instruments/readers/config_text_reader.yaml b/instruments/readers/config_text_reader.yaml index 79bfc2e..db00bed 100644 --- a/instruments/readers/config_text_reader.yaml +++ b/instruments/readers/config_text_reader.yaml @@ -1,112 +1,112 @@ -default: - file_encoding : 'utf-8' - separator : 'None' - table_header : 'None' - desired_format: '%Y-%m-%d %H:%M:%S.%f' - -RGA: - table_header : 'Time(s) Channel#1 Channel#2 Channel#3 Channel#4 Channel#5 Channel#6 Channel#7 Channel#8' - separator : '\t' - link_to_description: 'dictionaries/RGA.yaml' - -Pressure: - table_header : 'Date Time Vapore-Pressure 1 in Vapore-Pressure 2 in Baratron 1 in Baratron 2 in Baratron 3 in Baratron 4 in Temp. Ice-Sample in Temp. Heated-Sample in Temp. Cooler 1 in Temp. Cooler 2 in Flow Gas 1 in Pressure Chamber in X in Y in Z in None in Temp. Sealing in Flow Ice-Sample in' - separator : '\t' - timestamp: ['Date','Time'] - datetime_format: '%d.%m.%Y %H:%M:%S' - link_to_description: 'dictionaries/Preassure.yaml' - -Humidity_Sensors: - table_header : 'Date Time RH1[%] RH2[%] RH3[%] RH4[%] RH5[%] RH6[%] RH7[%] RH8[%] T1[°C] T2[°C] T3[°C] T4[°C] T5[°C] T6[°C] T7[°C] T8[°C] DP1[°C] DP2[°C] DP3[°C] DP4[°C] DP5[°C] DP6[°C] DP7[°C] DP8[°C]' - separator : '\t' - file_encoding : 'latin-1' - timestamp: ['Date','Time'] - datetime_format: '%d.%m.%Y %H:%M:%S' - link_to_description: 'dictionaries/Humidity_Sensors.yaml' - -HONO: #ICAD/HONO: - table_header : 'Start Date/Time (UTC) Duration (s) NO2 (ppb) NO2 Uncertainty (ppb) HONO (ppb) HONO Uncertainty (ppb) H2O (ppb) H2O Uncertainty (ppb) O4 (ppb) O4 Uncertainty (ppb) File Number Light Intensity #ICEDOAS iter. Cell Pressure Ambient Pressure Cell Temp Spec Temp Lat Lon Height Speed GPSQuality 0-Air Ref. Time 0-Air Ref. Duration 0-Air Ref. File Number 0-Air Ref. Intensity 0-Air Ref. Rel Intensity 0-Air Ref. Intensity valid MeasMode SampleSource' - separator : '\t' - file_encoding : 'latin-1' - timestamp: ['Start Date/Time (UTC)'] - datetime_format: '%Y-%m-%d %H:%M:%S.%f' - link_to_description: 'dictionaries/ICAD_HONO.yaml' - -NO2: #ICAD/NO2: - table_header : 'Start Date/Time (UTC) Duration (s) NO2 (ppb) NO2 Uncertainty (ppb) H2O (ppb) H2O Uncertainty (ppb) CHOCHO (ppb) CHOCHO Uncertainty (ppb) File Number Light Intensity #ICEDOAS iter. Cell Pressure Ambient Pressure Cell Temp Spec Temp Lat Lon Height Speed GPSQuality 0-Air Ref. Time 0-Air Ref. Duration 0-Air Ref. File Number 0-Air Ref. Intensity 0-Air Ref. Rel Intensity 0-Air Ref. Intensity valid MeasMode SampleSource' - separator : '\t' - file_encoding : 'latin-1' - timestamp: ['Start Date/Time (UTC)'] - datetime_format: '%Y-%m-%d %H:%M:%S.%f' - link_to_description: 'dictionaries/ICAD_NO2.yaml' - -Lopap: - #table_header : 'Date;Time;Ch1;490.1;500.2;510.0;520.0;530.1;540.0;550.7;603.2;700.3;800.0;Ch2;500.5;510.3;520.5;530.7;540.8;550.5;550.8;560.9;570.9;581.2;586.2;591.2;596.1;601.1;606.4;611.3;' - table_header : 'Date;Time;Ch1;' - separator : ';' - file_encoding : 'latin-1' - timestamp: ['Date','Time'] - datetime_format: '%d.%m.%Y %H:%M:%S' - link_to_description: 'dictionaries/Lopap.yaml' - -T200_NOx: - table_header : 'Date Time NO NO2 NOx' - separator : '\t' - file_encoding : 'latin-1' - timestamp: ['Date','Time'] - datetime_format: '%d.%m.%Y %H:%M:%S' - link_to_description: 'dictionaries/T200_NOx.yaml' - -T360U_CO2: - table_header : 'Date Time CO2' - separator : '\t' - file_encoding : 'latin-1' - timestamp: ['Date','Time'] - datetime_format: '%d.%m.%Y %H:%M:%S' - link_to_description: 'dictionaries/T360U_CO2.yaml' - -smps: - table_header: 'Sample # Date Start Time Sample Temp (C) Sample Pressure (kPa) Relative Humidity (%) Mean Free Path (m) Gas Viscosity (Pa*s) Diameter Midpoint (nm) 15.7 16.3 16.8 17.5 18.1 18.8 19.5 20.2 20.9 21.7 22.5 23.3 24.1 25.0 25.9 26.9 27.9 28.9 30.0 31.1 32.2 33.4 34.6 35.9 37.2 38.5 40.0 41.4 42.9 44.5 46.1 47.8 49.6 51.4 53.3 55.2 57.3 59.4 61.5 63.8 66.1 68.5 71.0 73.7 76.4 79.1 82.0 85.1 88.2 91.4 94.7 98.2 101.8 105.5 109.4 113.4 117.6 121.9 126.3 131.0 135.8 140.7 145.9 151.2 156.8 162.5 168.5 174.7 181.1 187.7 194.6 201.7 209.1 216.7 224.7 232.9 241.4 250.3 259.5 269.0 278.8 289.0 299.6 310.6 322.0 333.8 346.0 358.7 371.8 385.4 399.5 414.2 429.4 445.1 461.4 478.3 495.8 514.0 532.8 552.3 572.5 593.5 615.3 637.8 Scan Time (s) Retrace Time (s) Scan Resolution (Hz) Scans Per Sample Sheath Flow (L/min) Aerosol Flow (L/min) Bypass Flow (L/min) Low Voltage (V) High Voltage (V) Lower Size (nm) Upper Size (nm) Density (g/cm³) td + 0.5 (s) tf (s) D50 (nm) Neutralizer' - separator : '\t' - file_encoding : 'latin-1' - timestamp: ['Date','Start Time'] - datetime_format: '%d/%m/%Y %H:%M:%S' - link_to_description: 'dictionaries/smps.yaml' - -gas: - table_header : 'Date_Time HoribaNO HoribaNOy Thermo42C_NO Thermo42C_NOx APHA370 CH4 APHA370THC HygroclipRH HygroclipT ML9850SO2 ozone49c PAMrh PAMt xxxal xxxal xxxal xxxal ThermoCouple0 ThermoCouple1 ThermoCouple2 ThermoCouple3 xxxTC xxxTC xxxTC xxxTC xxxTC xxxTC xxxTC xxxTC xxxTC xxxTC xxxTC xxxTC CPC xxx LicorH2Odelta LicorCO2delta xxx 2BO2 xxx xxx HoribaCO xxx' - separator : '\t' - file_encoding : 'utf-8' - timestamp: ['Date_Time'] - datetime_format: '%Y.%m.%d %H:%M:%S' - link_to_description: 'dictionaries/gas.yaml' - -ACSM_TOFWARE: - table_header: - #txt: - - 't_base VaporizerTemp_C HeaterBias_V FlowRefWave FlowRate_mb FlowRate_ccs FilamentEmission_mA Detector_V AnalogInput06_V ABRefWave ABsamp ABCorrFact' - - 't_start_Buf,Chl_11000,NH4_11000,SO4_11000,NO3_11000,Org_11000,SO4_48_11000,SO4_62_11000,SO4_82_11000,SO4_81_11000,SO4_98_11000,NO3_30_11000,Org_60_11000,Org_43_11000,Org_44_11000' - #csv: - - "X4 X5 X6 X7 X8 X9 X10 X11 X12 X13 X14 X15 X16 X17 X18 X19 X20 X21 X22 X23 X24 X25 X26 X27 X28 X29 X30 X31 X32 X33 X34 X35 X36 X37 X38 X39 X40 X41 X42 X43 X44 X45 X46 X47 X48 X49 X50 X51 X52 X53 X54 X55 X56 X57 X58 X59 X60 X61 X62 X63 X64 X65 X66 X67 X68 X69 X70 X71 X72 X73 X74 X75 X76 X77 X78 X79 X80 X81 X82 X83 X84 X85 X86 X87 X88 X89 X90 X91 X92 X93 X94 X95 X96 X97 X98 X99 X100 X101 X102 X103 X104 X105 X106 X107 X108 X109 X110 X111 X112 X113 X114 X115 X116 X117 X118 X119 X120 X121 X122 X123 X124 X125 X126 X127 X128 X129 X130 X131 X132 X133 X134 X135 X136 X137 X138 X139 X140 X141 X142 X143 X144 X145 X146 X147 X148 X149 X150 X151 X152 X153 X154 X155 X156 X157 X158 X159 X160 X161 X162 X163 X164 X165 X166 X167 X168 X169 X170 X171 X172 X173 X174 X175 X176 X177 X178 X179 X180 X181 X182 X183 X184 X185 X186 X187 X188 X189 X190 X191 X192 X193 X194 X195 X196 X197 X198 X199 X200 X201 X202 X203 X204 X205 X206 X207 X208 X209 X210 X211 X212 X213 X214 X215 X216 X217 X218 X219" - - "X4 X5 X6 X7 X8 X9 X10 X11 X12 X13 X14 X15 X16 X17 X18 X19 X20 X21 X22 X23 X24 X25 X26 X27 X28 X29 X30 X31 X32 X33 X34 X35 X36 X37 X38 X39 X40 X41 X42 X43 X44 X45 X46 X47 X48 X49 X50 X51 X52 X53 X54 X55 X56 X57 X58 X59 X60 X61 X62 X63 X64 X65 X66 X67 X68 X69 X70 X71 X72 X73 X74 X75 X76 X77 X78 X79 X80 X81 X82 X83 X84 X85 X86 X87 X88 X89 X90 X91 X92 X93 X94 X95 X96 X97 X98 X99 X100 X101 X102 X103 X104 X105 X106 X107 X108 X109 X110 X111 X112 X113 X114 X115 X116 X117 X118 X119 X120 X121 X122 X123 X124 X125 X126 X127 X128 X129 X130 X131 X132 X133 X134 X135 X136 X137 X138 X139 X140 X141 X142 X143 X144 X145 X146 X147 X148 X149 X150 X151 X152 X153 X154 X155 X156 X157 X158 X159 X160 X161 X162 X163 X164 X165 X166 X167 X168 X169 X170 X171 X172 X173 X174 X175 X176 X177 X178 X179 X180 X181 X182 X183 X184 X185 X186 X187 X188 X189 X190 X191 X192 X193 X194 X195 X196 X197 X198 X199 X200 X201 X202 X203 X204 X205 X206 X207 X208 X209 X210 X211 X212 X213 X214 X215 X216 X217 X218 X219" - - 'MSS_base' - - 'tseries' - separator: - #txt: - - "\t" - - "," - #csv: - - "\t" - - "\t" - - "None" - - "None" - file_encoding: - #txt: - - "utf-8" - - "utf-8" - #csv: - - "utf-8" - - "utf-8" - - "utf-8" - - "utf-8" - +default: + file_encoding : 'utf-8' + separator : 'None' + table_header : 'None' + desired_format: '%Y-%m-%d %H:%M:%S.%f' + +RGA: + table_header : 'Time(s) Channel#1 Channel#2 Channel#3 Channel#4 Channel#5 Channel#6 Channel#7 Channel#8' + separator : '\t' + link_to_description: 'dictionaries/RGA.yaml' + +Pressure: + table_header : 'Date Time Vapore-Pressure 1 in Vapore-Pressure 2 in Baratron 1 in Baratron 2 in Baratron 3 in Baratron 4 in Temp. Ice-Sample in Temp. Heated-Sample in Temp. Cooler 1 in Temp. Cooler 2 in Flow Gas 1 in Pressure Chamber in X in Y in Z in None in Temp. Sealing in Flow Ice-Sample in' + separator : '\t' + timestamp: ['Date','Time'] + datetime_format: '%d.%m.%Y %H:%M:%S' + link_to_description: 'dictionaries/Preassure.yaml' + +Humidity_Sensors: + table_header : 'Date Time RH1[%] RH2[%] RH3[%] RH4[%] RH5[%] RH6[%] RH7[%] RH8[%] T1[°C] T2[°C] T3[°C] T4[°C] T5[°C] T6[°C] T7[°C] T8[°C] DP1[°C] DP2[°C] DP3[°C] DP4[°C] DP5[°C] DP6[°C] DP7[°C] DP8[°C]' + separator : '\t' + file_encoding : 'latin-1' + timestamp: ['Date','Time'] + datetime_format: '%d.%m.%Y %H:%M:%S' + link_to_description: 'dictionaries/Humidity_Sensors.yaml' + +HONO: #ICAD/HONO: + table_header : 'Start Date/Time (UTC) Duration (s) NO2 (ppb) NO2 Uncertainty (ppb) HONO (ppb) HONO Uncertainty (ppb) H2O (ppb) H2O Uncertainty (ppb) O4 (ppb) O4 Uncertainty (ppb) File Number Light Intensity #ICEDOAS iter. Cell Pressure Ambient Pressure Cell Temp Spec Temp Lat Lon Height Speed GPSQuality 0-Air Ref. Time 0-Air Ref. Duration 0-Air Ref. File Number 0-Air Ref. Intensity 0-Air Ref. Rel Intensity 0-Air Ref. Intensity valid MeasMode SampleSource' + separator : '\t' + file_encoding : 'latin-1' + timestamp: ['Start Date/Time (UTC)'] + datetime_format: '%Y-%m-%d %H:%M:%S.%f' + link_to_description: 'dictionaries/ICAD_HONO.yaml' + +NO2: #ICAD/NO2: + table_header : 'Start Date/Time (UTC) Duration (s) NO2 (ppb) NO2 Uncertainty (ppb) H2O (ppb) H2O Uncertainty (ppb) CHOCHO (ppb) CHOCHO Uncertainty (ppb) File Number Light Intensity #ICEDOAS iter. Cell Pressure Ambient Pressure Cell Temp Spec Temp Lat Lon Height Speed GPSQuality 0-Air Ref. Time 0-Air Ref. Duration 0-Air Ref. File Number 0-Air Ref. Intensity 0-Air Ref. Rel Intensity 0-Air Ref. Intensity valid MeasMode SampleSource' + separator : '\t' + file_encoding : 'latin-1' + timestamp: ['Start Date/Time (UTC)'] + datetime_format: '%Y-%m-%d %H:%M:%S.%f' + link_to_description: 'dictionaries/ICAD_NO2.yaml' + +Lopap: + #table_header : 'Date;Time;Ch1;490.1;500.2;510.0;520.0;530.1;540.0;550.7;603.2;700.3;800.0;Ch2;500.5;510.3;520.5;530.7;540.8;550.5;550.8;560.9;570.9;581.2;586.2;591.2;596.1;601.1;606.4;611.3;' + table_header : 'Date;Time;Ch1;' + separator : ';' + file_encoding : 'latin-1' + timestamp: ['Date','Time'] + datetime_format: '%d.%m.%Y %H:%M:%S' + link_to_description: 'dictionaries/Lopap.yaml' + +T200_NOx: + table_header : 'Date Time NO NO2 NOx' + separator : '\t' + file_encoding : 'latin-1' + timestamp: ['Date','Time'] + datetime_format: '%d.%m.%Y %H:%M:%S' + link_to_description: 'dictionaries/T200_NOx.yaml' + +T360U_CO2: + table_header : 'Date Time CO2' + separator : '\t' + file_encoding : 'latin-1' + timestamp: ['Date','Time'] + datetime_format: '%d.%m.%Y %H:%M:%S' + link_to_description: 'dictionaries/T360U_CO2.yaml' + +smps: + table_header: 'Sample # Date Start Time Sample Temp (C) Sample Pressure (kPa) Relative Humidity (%) Mean Free Path (m) Gas Viscosity (Pa*s) Diameter Midpoint (nm) 15.7 16.3 16.8 17.5 18.1 18.8 19.5 20.2 20.9 21.7 22.5 23.3 24.1 25.0 25.9 26.9 27.9 28.9 30.0 31.1 32.2 33.4 34.6 35.9 37.2 38.5 40.0 41.4 42.9 44.5 46.1 47.8 49.6 51.4 53.3 55.2 57.3 59.4 61.5 63.8 66.1 68.5 71.0 73.7 76.4 79.1 82.0 85.1 88.2 91.4 94.7 98.2 101.8 105.5 109.4 113.4 117.6 121.9 126.3 131.0 135.8 140.7 145.9 151.2 156.8 162.5 168.5 174.7 181.1 187.7 194.6 201.7 209.1 216.7 224.7 232.9 241.4 250.3 259.5 269.0 278.8 289.0 299.6 310.6 322.0 333.8 346.0 358.7 371.8 385.4 399.5 414.2 429.4 445.1 461.4 478.3 495.8 514.0 532.8 552.3 572.5 593.5 615.3 637.8 Scan Time (s) Retrace Time (s) Scan Resolution (Hz) Scans Per Sample Sheath Flow (L/min) Aerosol Flow (L/min) Bypass Flow (L/min) Low Voltage (V) High Voltage (V) Lower Size (nm) Upper Size (nm) Density (g/cm³) td + 0.5 (s) tf (s) D50 (nm) Neutralizer' + separator : '\t' + file_encoding : 'latin-1' + timestamp: ['Date','Start Time'] + datetime_format: '%d/%m/%Y %H:%M:%S' + link_to_description: 'dictionaries/smps.yaml' + +gas: + table_header : 'Date_Time HoribaNO HoribaNOy Thermo42C_NO Thermo42C_NOx APHA370 CH4 APHA370THC HygroclipRH HygroclipT ML9850SO2 ozone49c PAMrh PAMt xxxal xxxal xxxal xxxal ThermoCouple0 ThermoCouple1 ThermoCouple2 ThermoCouple3 xxxTC xxxTC xxxTC xxxTC xxxTC xxxTC xxxTC xxxTC xxxTC xxxTC xxxTC xxxTC CPC xxx LicorH2Odelta LicorCO2delta xxx 2BO2 xxx xxx HoribaCO xxx' + separator : '\t' + file_encoding : 'utf-8' + timestamp: ['Date_Time'] + datetime_format: '%Y.%m.%d %H:%M:%S' + link_to_description: 'dictionaries/gas.yaml' + +ACSM_TOFWARE: + table_header: + #txt: + - 't_base VaporizerTemp_C HeaterBias_V FlowRefWave FlowRate_mb FlowRate_ccs FilamentEmission_mA Detector_V AnalogInput06_V ABRefWave ABsamp ABCorrFact' + - 't_start_Buf,Chl_11000,NH4_11000,SO4_11000,NO3_11000,Org_11000,SO4_48_11000,SO4_62_11000,SO4_82_11000,SO4_81_11000,SO4_98_11000,NO3_30_11000,Org_60_11000,Org_43_11000,Org_44_11000' + #csv: + - "X4 X5 X6 X7 X8 X9 X10 X11 X12 X13 X14 X15 X16 X17 X18 X19 X20 X21 X22 X23 X24 X25 X26 X27 X28 X29 X30 X31 X32 X33 X34 X35 X36 X37 X38 X39 X40 X41 X42 X43 X44 X45 X46 X47 X48 X49 X50 X51 X52 X53 X54 X55 X56 X57 X58 X59 X60 X61 X62 X63 X64 X65 X66 X67 X68 X69 X70 X71 X72 X73 X74 X75 X76 X77 X78 X79 X80 X81 X82 X83 X84 X85 X86 X87 X88 X89 X90 X91 X92 X93 X94 X95 X96 X97 X98 X99 X100 X101 X102 X103 X104 X105 X106 X107 X108 X109 X110 X111 X112 X113 X114 X115 X116 X117 X118 X119 X120 X121 X122 X123 X124 X125 X126 X127 X128 X129 X130 X131 X132 X133 X134 X135 X136 X137 X138 X139 X140 X141 X142 X143 X144 X145 X146 X147 X148 X149 X150 X151 X152 X153 X154 X155 X156 X157 X158 X159 X160 X161 X162 X163 X164 X165 X166 X167 X168 X169 X170 X171 X172 X173 X174 X175 X176 X177 X178 X179 X180 X181 X182 X183 X184 X185 X186 X187 X188 X189 X190 X191 X192 X193 X194 X195 X196 X197 X198 X199 X200 X201 X202 X203 X204 X205 X206 X207 X208 X209 X210 X211 X212 X213 X214 X215 X216 X217 X218 X219" + - "X4 X5 X6 X7 X8 X9 X10 X11 X12 X13 X14 X15 X16 X17 X18 X19 X20 X21 X22 X23 X24 X25 X26 X27 X28 X29 X30 X31 X32 X33 X34 X35 X36 X37 X38 X39 X40 X41 X42 X43 X44 X45 X46 X47 X48 X49 X50 X51 X52 X53 X54 X55 X56 X57 X58 X59 X60 X61 X62 X63 X64 X65 X66 X67 X68 X69 X70 X71 X72 X73 X74 X75 X76 X77 X78 X79 X80 X81 X82 X83 X84 X85 X86 X87 X88 X89 X90 X91 X92 X93 X94 X95 X96 X97 X98 X99 X100 X101 X102 X103 X104 X105 X106 X107 X108 X109 X110 X111 X112 X113 X114 X115 X116 X117 X118 X119 X120 X121 X122 X123 X124 X125 X126 X127 X128 X129 X130 X131 X132 X133 X134 X135 X136 X137 X138 X139 X140 X141 X142 X143 X144 X145 X146 X147 X148 X149 X150 X151 X152 X153 X154 X155 X156 X157 X158 X159 X160 X161 X162 X163 X164 X165 X166 X167 X168 X169 X170 X171 X172 X173 X174 X175 X176 X177 X178 X179 X180 X181 X182 X183 X184 X185 X186 X187 X188 X189 X190 X191 X192 X193 X194 X195 X196 X197 X198 X199 X200 X201 X202 X203 X204 X205 X206 X207 X208 X209 X210 X211 X212 X213 X214 X215 X216 X217 X218 X219" + - 'MSS_base' + - 'tseries' + separator: + #txt: + - "\t" + - "," + #csv: + - "\t" + - "\t" + - "None" + - "None" + file_encoding: + #txt: + - "utf-8" + - "utf-8" + #csv: + - "utf-8" + - "utf-8" + - "utf-8" + - "utf-8" + diff --git a/instruments/readers/filereader_registry.py b/instruments/readers/filereader_registry.py index 87362e7..a9c4774 100644 --- a/instruments/readers/filereader_registry.py +++ b/instruments/readers/filereader_registry.py @@ -1,80 +1,80 @@ -import os -import sys -#root_dir = os.path.abspath(os.curdir) -#sys.path.append(root_dir) - -from instruments.readers.xps_ibw_reader import read_xps_ibw_file_as_dict -from instruments.readers.g5505_text_reader import read_txt_files_as_dict - - -file_extensions = ['.ibw','.txt','.dat','.h5','.TXT','.csv','.pkl','.json','.yaml'] - -# Define the instruments directory (modify this as needed or set to None) -default_instruments_dir = None # or provide an absolute path - -file_readers = { - 'ibw': lambda a1: read_xps_ibw_file_as_dict(a1), - 'txt': lambda a1: read_txt_files_as_dict(a1, instruments_dir=default_instruments_dir, work_with_copy=False), - 'TXT': lambda a1: read_txt_files_as_dict(a1, instruments_dir=default_instruments_dir, work_with_copy=False), - 'dat': lambda a1: read_txt_files_as_dict(a1, instruments_dir=default_instruments_dir, work_with_copy=False), - #'ACSM_TOFWARE_txt': lambda a1: read_txt_files_as_dict(a1, instruments_dir=default_instruments_dir, work_with_copy=False), - #'ACSM_TOFWARE_csv': lambda a1: read_txt_files_as_dict(a1, instruments_dir=default_instruments_dir, work_with_copy=False) -} - -# Add new "instrument reader (Data flagging app data)" - -from instruments.readers.acsm_tofware_reader import read_acsm_files_as_dict -file_extensions.append('.txt') -file_readers.update({'ACSM_TOFWARE_txt' : lambda x: read_acsm_files_as_dict(x, instruments_dir=default_instruments_dir, work_with_copy=False)}) - -file_extensions.append('.csv') -file_readers.update({'ACSM_TOFWARE_csv' : lambda x: read_acsm_files_as_dict(x, instruments_dir=default_instruments_dir, work_with_copy=False)}) - -from instruments.readers.flag_reader import read_jsonflag_as_dict -file_extensions.append('.json') -file_readers.update({'ACSM_TOFWARE_flags_json' : lambda x: read_jsonflag_as_dict(x)}) - -def compute_filereader_key_from_path(hdf5_file_path): - """Constructs the key 'instrumentname_ext' based on hdf5_file_path, structured as - /instrumentname/to/filename.ext, which access the file reader that should be used to read such a file. - - Parameters - ---------- - hdf5_file_path : str - _description_ - - Returns - ------- - _type_ - _description_ - """ - - parts = hdf5_file_path.strip('/').split('/') - - # Extract the filename and its extension - filename, file_extension = os.path.splitext(parts[-1]) - - # Extract the first directory directly under the root directory '/' in the hdf5 file - subfolder_name = parts[0] if len(parts) > 1 else "" - - # Remove leading dot from the file extension - file_extension = file_extension.lstrip('.') - - # Construct the resulting string - full_string = f"{subfolder_name}_{file_extension}" - - return full_string, file_extension - -def select_file_reader(path): - full_string, extension = compute_filereader_key_from_path(path) - - # First, try to match the full string - if full_string in file_readers: - return file_readers[full_string] - - # If no match, try to match the reader using only the extension - if extension in file_readers: - return file_readers[extension] - - # Default case if no reader is found +import os +import sys +#root_dir = os.path.abspath(os.curdir) +#sys.path.append(root_dir) + +from instruments.readers.xps_ibw_reader import read_xps_ibw_file_as_dict +from instruments.readers.g5505_text_reader import read_txt_files_as_dict + + +file_extensions = ['.ibw','.txt','.dat','.h5','.TXT','.csv','.pkl','.json','.yaml'] + +# Define the instruments directory (modify this as needed or set to None) +default_instruments_dir = None # or provide an absolute path + +file_readers = { + 'ibw': lambda a1: read_xps_ibw_file_as_dict(a1), + 'txt': lambda a1: read_txt_files_as_dict(a1, instruments_dir=default_instruments_dir, work_with_copy=False), + 'TXT': lambda a1: read_txt_files_as_dict(a1, instruments_dir=default_instruments_dir, work_with_copy=False), + 'dat': lambda a1: read_txt_files_as_dict(a1, instruments_dir=default_instruments_dir, work_with_copy=False), + #'ACSM_TOFWARE_txt': lambda a1: read_txt_files_as_dict(a1, instruments_dir=default_instruments_dir, work_with_copy=False), + #'ACSM_TOFWARE_csv': lambda a1: read_txt_files_as_dict(a1, instruments_dir=default_instruments_dir, work_with_copy=False) +} + +# Add new "instrument reader (Data flagging app data)" + +from instruments.readers.acsm_tofware_reader import read_acsm_files_as_dict +file_extensions.append('.txt') +file_readers.update({'ACSM_TOFWARE_txt' : lambda x: read_acsm_files_as_dict(x, instruments_dir=default_instruments_dir, work_with_copy=False)}) + +file_extensions.append('.csv') +file_readers.update({'ACSM_TOFWARE_csv' : lambda x: read_acsm_files_as_dict(x, instruments_dir=default_instruments_dir, work_with_copy=False)}) + +from instruments.readers.flag_reader import read_jsonflag_as_dict +file_extensions.append('.json') +file_readers.update({'ACSM_TOFWARE_flags_json' : lambda x: read_jsonflag_as_dict(x)}) + +def compute_filereader_key_from_path(hdf5_file_path): + """Constructs the key 'instrumentname_ext' based on hdf5_file_path, structured as + /instrumentname/to/filename.ext, which access the file reader that should be used to read such a file. + + Parameters + ---------- + hdf5_file_path : str + _description_ + + Returns + ------- + _type_ + _description_ + """ + + parts = hdf5_file_path.strip('/').split('/') + + # Extract the filename and its extension + filename, file_extension = os.path.splitext(parts[-1]) + + # Extract the first directory directly under the root directory '/' in the hdf5 file + subfolder_name = parts[0] if len(parts) > 1 else "" + + # Remove leading dot from the file extension + file_extension = file_extension.lstrip('.') + + # Construct the resulting string + full_string = f"{subfolder_name}_{file_extension}" + + return full_string, file_extension + +def select_file_reader(path): + full_string, extension = compute_filereader_key_from_path(path) + + # First, try to match the full string + if full_string in file_readers: + return file_readers[full_string] + + # If no match, try to match the reader using only the extension + if extension in file_readers: + return file_readers[extension] + + # Default case if no reader is found return lambda x : None \ No newline at end of file diff --git a/instruments/readers/flag_reader.py b/instruments/readers/flag_reader.py index cbcebaf..8534bb4 100644 --- a/instruments/readers/flag_reader.py +++ b/instruments/readers/flag_reader.py @@ -1,39 +1,39 @@ -import os -import json - -#root_dir = os.path.abspath(os.curdir) -#sys.path.append(root_dir) -#print(__file__) - -#from instruments.readers import set_dima_path as configpath -#configpath.set_dima_path() - -from utils import g5505_utils - - -def read_jsonflag_as_dict(path_to_file): - - - file_dict = {} - path_tail, path_head = os.path.split(path_to_file) - - file_dict['name'] = path_head - # TODO: review this header dictionary, it may not be the best way to represent header data - file_dict['attributes_dict'] = {} - file_dict['datasets'] = [] - - try: - with open(path_to_file, 'r') as stream: - flag = json.load(stream)#, Loader=json.FullLoader) - except (FileNotFoundError, json.JSONDecodeError) as exc: - print(exc) - - dataset = {} - dataset['name'] = 'data_table'#_numerical_variables' - dataset['data'] = g5505_utils.convert_attrdict_to_np_structured_array(flag) #df_numerical_attrs.to_numpy() - dataset['shape'] = dataset['data'].shape - dataset['dtype'] = type(dataset['data']) - - file_dict['datasets'].append(dataset) - +import os +import json + +#root_dir = os.path.abspath(os.curdir) +#sys.path.append(root_dir) +#print(__file__) + +#from instruments.readers import set_dima_path as configpath +#configpath.set_dima_path() + +from utils import g5505_utils + + +def read_jsonflag_as_dict(path_to_file): + + + file_dict = {} + path_tail, path_head = os.path.split(path_to_file) + + file_dict['name'] = path_head + # TODO: review this header dictionary, it may not be the best way to represent header data + file_dict['attributes_dict'] = {} + file_dict['datasets'] = [] + + try: + with open(path_to_file, 'r') as stream: + flag = json.load(stream)#, Loader=json.FullLoader) + except (FileNotFoundError, json.JSONDecodeError) as exc: + print(exc) + + dataset = {} + dataset['name'] = 'data_table'#_numerical_variables' + dataset['data'] = g5505_utils.convert_attrdict_to_np_structured_array(flag) #df_numerical_attrs.to_numpy() + dataset['shape'] = dataset['data'].shape + dataset['dtype'] = type(dataset['data']) + + file_dict['datasets'].append(dataset) + return file_dict \ No newline at end of file diff --git a/instruments/readers/g5505_text_reader.py b/instruments/readers/g5505_text_reader.py index c4eab07..ed019e0 100644 --- a/instruments/readers/g5505_text_reader.py +++ b/instruments/readers/g5505_text_reader.py @@ -1,239 +1,239 @@ -import sys -import os -import pandas as pd -import collections -import yaml - -# Import project modules -root_dir = os.path.abspath(os.curdir) -sys.path.append(root_dir) - -import utils.g5505_utils as utils - - - - -def read_txt_files_as_dict(filename: str, instruments_dir: str = None, work_with_copy: bool = True): - # If instruments_dir is not provided, use the default path relative to the module directory - if not instruments_dir: - # Assuming the instruments folder is one level up from the source module directory - module_dir = os.path.dirname(__file__) - instruments_dir = os.path.join(module_dir, '..') - - # Normalize the path (resolves any '..' in the path) - instrument_configs_path = os.path.abspath(os.path.join(instruments_dir,'readers','config_text_reader.yaml')) - - with open(instrument_configs_path,'r') as stream: - try: - config_dict = yaml.load(stream, Loader=yaml.FullLoader) - except yaml.YAMLError as exc: - print(exc) - # Verify if file can be read by available intrument configurations. - #if not any(key in filename.replace(os.sep,'/') for key in config_dict.keys()): - # return {} - - - #TODO: this may be prone to error if assumed folder structure is non compliant - file_encoding = config_dict['default']['file_encoding'] #'utf-8' - separator = config_dict['default']['separator'] - table_header = config_dict['default']['table_header'] - - for key in config_dict.keys(): - if key.replace('/',os.sep) in filename: - file_encoding = config_dict[key].get('file_encoding',file_encoding) - separator = config_dict[key].get('separator',separator) - table_header = config_dict[key].get('table_header',table_header) - timestamp_variables = config_dict[key].get('timestamp',[]) - datetime_format = config_dict[key].get('datetime_format',[]) - - description_dict = {} - link_to_description = config_dict[key].get('link_to_description', '').replace('/', os.sep) - - if link_to_description: - path = os.path.join(instruments_dir, link_to_description) - try: - with open(path, 'r') as stream: - description_dict = yaml.load(stream, Loader=yaml.FullLoader) - except (FileNotFoundError, yaml.YAMLError) as exc: - print(exc) - #if 'None' in table_header: - # return {} - - # Read header as a dictionary and detect where data table starts - header_dict = {} - data_start = False - # Work with copy of the file for safety - if work_with_copy: - tmp_filename = utils.make_file_copy(source_file_path=filename) - else: - tmp_filename = filename - - #with open(tmp_filename,'rb',encoding=file_encoding,errors='ignore') as f: - - if not isinstance(table_header, list): - table_header = [table_header] - file_encoding = [file_encoding] - separator = [separator] - - with open(tmp_filename,'rb') as f: - table_preamble = [] - for line_number, line in enumerate(f): - - - for tb_idx, tb in enumerate(table_header): - if tb in line.decode(file_encoding[tb_idx]): - break - - if tb in line.decode(file_encoding[tb_idx]): - list_of_substrings = line.decode(file_encoding[tb_idx]).split(separator[tb_idx].replace('\\t','\t')) - - # Count occurrences of each substring - substring_counts = collections.Counter(list_of_substrings) - data_start = True - # Generate column names with appended index only for repeated substrings - column_names = [f"{i}_{name.strip()}" if substring_counts[name] > 1 else name.strip() for i, name in enumerate(list_of_substrings)] - - #column_names = [str(i)+'_'+name.strip() for i, name in enumerate(list_of_substrings)] - #column_names = [] - #for i, name in enumerate(list_of_substrings): - # column_names.append(str(i)+'_'+name) - - #print(line_number, len(column_names ),'\n') - break - # Subdivide line into words, and join them by single space. - # I asumme this can produce a cleaner line that contains no weird separator characters \t \r or extra spaces and so on. - list_of_substrings = line.decode(file_encoding[tb_idx]).split() - # TODO: ideally we should use a multilinear string but the yalm parser is not recognizing \n as special character - #line = ' '.join(list_of_substrings+['\n']) - #line = ' '.join(list_of_substrings) - table_preamble.append(' '.join([item for item in list_of_substrings]))# += new_line - - - # TODO: it does not work with separator as none :(. fix for RGA - try: - df = pd.read_csv(tmp_filename, - delimiter = separator[tb_idx].replace('\\t','\t'), - header=line_number, - #encoding='latin-1', - encoding = file_encoding[tb_idx], - names=column_names, - skip_blank_lines=True) - - df_numerical_attrs = df.select_dtypes(include ='number') - df_categorical_attrs = df.select_dtypes(exclude='number') - numerical_variables = [item for item in df_numerical_attrs.columns] - - # Consolidate into single timestamp column the separate columns 'date' 'time' specified in text_data_source.yaml - if timestamp_variables: - #df_categorical_attrs['timestamps'] = [' '.join(df_categorical_attrs.loc[i,timestamp_variables].to_numpy()) for i in df.index] - #df_categorical_attrs['timestamps'] = [ df_categorical_attrs.loc[i,'0_Date']+' '+df_categorical_attrs.loc[i,'1_Time'] for i in df.index] - - - #df_categorical_attrs['timestamps'] = df_categorical_attrs[timestamp_variables].astype(str).agg(' '.join, axis=1) - timestamps_name = ' '.join(timestamp_variables) - df_categorical_attrs[ timestamps_name] = df_categorical_attrs[timestamp_variables].astype(str).agg(' '.join, axis=1) - - valid_indices = [] - if datetime_format: - df_categorical_attrs[ timestamps_name] = pd.to_datetime(df_categorical_attrs[ timestamps_name],format=datetime_format,errors='coerce') - valid_indices = df_categorical_attrs.dropna(subset=[timestamps_name]).index - df_categorical_attrs = df_categorical_attrs.loc[valid_indices,:] - df_numerical_attrs = df_numerical_attrs.loc[valid_indices,:] - - df_categorical_attrs[timestamps_name] = df_categorical_attrs[timestamps_name].dt.strftime(config_dict['default']['desired_format']) - startdate = df_categorical_attrs[timestamps_name].min() - enddate = df_categorical_attrs[timestamps_name].max() - - df_categorical_attrs[timestamps_name] = df_categorical_attrs[timestamps_name].astype(str) - #header_dict.update({'stastrrtdate':startdate,'enddate':enddate}) - header_dict['startdate']= str(startdate) - header_dict['enddate']=str(enddate) - - if len(timestamp_variables) > 1: - df_categorical_attrs = df_categorical_attrs.drop(columns = timestamp_variables) - - - #df_categorical_attrs.reindex(drop=True) - #df_numerical_attrs.reindex(drop=True) - - - - categorical_variables = [item for item in df_categorical_attrs.columns] - #### - #elif 'RGA' in filename: - # df_categorical_attrs = df_categorical_attrs.rename(columns={'0_Time(s)' : 'timestamps'}) - - ### - file_dict = {} - path_tail, path_head = os.path.split(tmp_filename) - - file_dict['name'] = path_head - # TODO: review this header dictionary, it may not be the best way to represent header data - file_dict['attributes_dict'] = header_dict - file_dict['datasets'] = [] - #### - - df = pd.concat((df_categorical_attrs,df_numerical_attrs),axis=1) - - #if numerical_variables: - dataset = {} - dataset['name'] = 'data_table'#_numerical_variables' - dataset['data'] = utils.convert_dataframe_to_np_structured_array(df) #df_numerical_attrs.to_numpy() - dataset['shape'] = dataset['data'].shape - dataset['dtype'] = type(dataset['data']) - #dataset['data_units'] = file_obj['wave']['data_units'] - # - # Create attribute descriptions based on description_dict - dataset['attributes'] = {} - - # Annotate column headers if description_dict is non empty - if description_dict: - for column_name in df.columns: - column_attr_dict = description_dict['table_header'].get(column_name, - {'note':'there was no description available. Review instrument files.'}) - dataset['attributes'].update({column_name: utils.convert_attrdict_to_np_structured_array(column_attr_dict)}) - - #try: - # dataset['attributes'] = description_dict['table_header'].copy() - # for key in description_dict['table_header'].keys(): - # if not key in numerical_variables: - # dataset['attributes'].pop(key) # delete key - # else: - # dataset['attributes'][key] = utils.parse_attribute(dataset['attributes'][key]) - # if timestamps_name in categorical_variables: - # dataset['attributes'][timestamps_name] = utils.parse_attribute({'unit':'YYYY-MM-DD HH:MM:SS.ffffff'}) - #except ValueError as err: - # print(err) - - # Represent string values as fixed length strings in the HDF5 file, which need - # to be decoded as string when we read them. It provides better control than variable strings, - # at the expense of flexibility. - # https://docs.h5py.org/en/stable/strings.html - - - if table_preamble: - #header_dict["table_preamble"] = utils.convert_string_to_bytes(table_preamble) - tp_dataset = {} - tp_dataset['name'] = "table_preamble" - tp_dataset['data'] = utils.convert_string_to_bytes(table_preamble) - tp_dataset['shape'] = tp_dataset['data'].shape - tp_dataset['dtype'] = type(tp_dataset['data']) - tp_dataset['attributes'] = {} - file_dict['datasets'].append(tp_dataset) - - file_dict['datasets'].append(dataset) - - - #if categorical_variables: - # dataset = {} - # dataset['name'] = 'table_categorical_variables' - # dataset['data'] = dataframe_to_np_structured_array(df_categorical_attrs) #df_categorical_attrs.loc[:,categorical_variables].to_numpy() - # dataset['shape'] = dataset['data'].shape - # dataset['dtype'] = type(dataset['data']) - # if timestamps_name in categorical_variables: - # dataset['attributes'] = {timestamps_name: utils.parse_attribute({'unit':'YYYY-MM-DD HH:MM:SS.ffffff'})} - # file_dict['datasets'].append(dataset) - except: - return {} - +import sys +import os +import pandas as pd +import collections +import yaml + +# Import project modules +root_dir = os.path.abspath(os.curdir) +sys.path.append(root_dir) + +import utils.g5505_utils as utils + + + + +def read_txt_files_as_dict(filename: str, instruments_dir: str = None, work_with_copy: bool = True): + # If instruments_dir is not provided, use the default path relative to the module directory + if not instruments_dir: + # Assuming the instruments folder is one level up from the source module directory + module_dir = os.path.dirname(__file__) + instruments_dir = os.path.join(module_dir, '..') + + # Normalize the path (resolves any '..' in the path) + instrument_configs_path = os.path.abspath(os.path.join(instruments_dir,'readers','config_text_reader.yaml')) + + with open(instrument_configs_path,'r') as stream: + try: + config_dict = yaml.load(stream, Loader=yaml.FullLoader) + except yaml.YAMLError as exc: + print(exc) + # Verify if file can be read by available intrument configurations. + #if not any(key in filename.replace(os.sep,'/') for key in config_dict.keys()): + # return {} + + + #TODO: this may be prone to error if assumed folder structure is non compliant + file_encoding = config_dict['default']['file_encoding'] #'utf-8' + separator = config_dict['default']['separator'] + table_header = config_dict['default']['table_header'] + + for key in config_dict.keys(): + if key.replace('/',os.sep) in filename: + file_encoding = config_dict[key].get('file_encoding',file_encoding) + separator = config_dict[key].get('separator',separator) + table_header = config_dict[key].get('table_header',table_header) + timestamp_variables = config_dict[key].get('timestamp',[]) + datetime_format = config_dict[key].get('datetime_format',[]) + + description_dict = {} + link_to_description = config_dict[key].get('link_to_description', '').replace('/', os.sep) + + if link_to_description: + path = os.path.join(instruments_dir, link_to_description) + try: + with open(path, 'r') as stream: + description_dict = yaml.load(stream, Loader=yaml.FullLoader) + except (FileNotFoundError, yaml.YAMLError) as exc: + print(exc) + #if 'None' in table_header: + # return {} + + # Read header as a dictionary and detect where data table starts + header_dict = {} + data_start = False + # Work with copy of the file for safety + if work_with_copy: + tmp_filename = utils.make_file_copy(source_file_path=filename) + else: + tmp_filename = filename + + #with open(tmp_filename,'rb',encoding=file_encoding,errors='ignore') as f: + + if not isinstance(table_header, list): + table_header = [table_header] + file_encoding = [file_encoding] + separator = [separator] + + with open(tmp_filename,'rb') as f: + table_preamble = [] + for line_number, line in enumerate(f): + + + for tb_idx, tb in enumerate(table_header): + if tb in line.decode(file_encoding[tb_idx]): + break + + if tb in line.decode(file_encoding[tb_idx]): + list_of_substrings = line.decode(file_encoding[tb_idx]).split(separator[tb_idx].replace('\\t','\t')) + + # Count occurrences of each substring + substring_counts = collections.Counter(list_of_substrings) + data_start = True + # Generate column names with appended index only for repeated substrings + column_names = [f"{i}_{name.strip()}" if substring_counts[name] > 1 else name.strip() for i, name in enumerate(list_of_substrings)] + + #column_names = [str(i)+'_'+name.strip() for i, name in enumerate(list_of_substrings)] + #column_names = [] + #for i, name in enumerate(list_of_substrings): + # column_names.append(str(i)+'_'+name) + + #print(line_number, len(column_names ),'\n') + break + # Subdivide line into words, and join them by single space. + # I asumme this can produce a cleaner line that contains no weird separator characters \t \r or extra spaces and so on. + list_of_substrings = line.decode(file_encoding[tb_idx]).split() + # TODO: ideally we should use a multilinear string but the yalm parser is not recognizing \n as special character + #line = ' '.join(list_of_substrings+['\n']) + #line = ' '.join(list_of_substrings) + table_preamble.append(' '.join([item for item in list_of_substrings]))# += new_line + + + # TODO: it does not work with separator as none :(. fix for RGA + try: + df = pd.read_csv(tmp_filename, + delimiter = separator[tb_idx].replace('\\t','\t'), + header=line_number, + #encoding='latin-1', + encoding = file_encoding[tb_idx], + names=column_names, + skip_blank_lines=True) + + df_numerical_attrs = df.select_dtypes(include ='number') + df_categorical_attrs = df.select_dtypes(exclude='number') + numerical_variables = [item for item in df_numerical_attrs.columns] + + # Consolidate into single timestamp column the separate columns 'date' 'time' specified in text_data_source.yaml + if timestamp_variables: + #df_categorical_attrs['timestamps'] = [' '.join(df_categorical_attrs.loc[i,timestamp_variables].to_numpy()) for i in df.index] + #df_categorical_attrs['timestamps'] = [ df_categorical_attrs.loc[i,'0_Date']+' '+df_categorical_attrs.loc[i,'1_Time'] for i in df.index] + + + #df_categorical_attrs['timestamps'] = df_categorical_attrs[timestamp_variables].astype(str).agg(' '.join, axis=1) + timestamps_name = ' '.join(timestamp_variables) + df_categorical_attrs[ timestamps_name] = df_categorical_attrs[timestamp_variables].astype(str).agg(' '.join, axis=1) + + valid_indices = [] + if datetime_format: + df_categorical_attrs[ timestamps_name] = pd.to_datetime(df_categorical_attrs[ timestamps_name],format=datetime_format,errors='coerce') + valid_indices = df_categorical_attrs.dropna(subset=[timestamps_name]).index + df_categorical_attrs = df_categorical_attrs.loc[valid_indices,:] + df_numerical_attrs = df_numerical_attrs.loc[valid_indices,:] + + df_categorical_attrs[timestamps_name] = df_categorical_attrs[timestamps_name].dt.strftime(config_dict['default']['desired_format']) + startdate = df_categorical_attrs[timestamps_name].min() + enddate = df_categorical_attrs[timestamps_name].max() + + df_categorical_attrs[timestamps_name] = df_categorical_attrs[timestamps_name].astype(str) + #header_dict.update({'stastrrtdate':startdate,'enddate':enddate}) + header_dict['startdate']= str(startdate) + header_dict['enddate']=str(enddate) + + if len(timestamp_variables) > 1: + df_categorical_attrs = df_categorical_attrs.drop(columns = timestamp_variables) + + + #df_categorical_attrs.reindex(drop=True) + #df_numerical_attrs.reindex(drop=True) + + + + categorical_variables = [item for item in df_categorical_attrs.columns] + #### + #elif 'RGA' in filename: + # df_categorical_attrs = df_categorical_attrs.rename(columns={'0_Time(s)' : 'timestamps'}) + + ### + file_dict = {} + path_tail, path_head = os.path.split(tmp_filename) + + file_dict['name'] = path_head + # TODO: review this header dictionary, it may not be the best way to represent header data + file_dict['attributes_dict'] = header_dict + file_dict['datasets'] = [] + #### + + df = pd.concat((df_categorical_attrs,df_numerical_attrs),axis=1) + + #if numerical_variables: + dataset = {} + dataset['name'] = 'data_table'#_numerical_variables' + dataset['data'] = utils.convert_dataframe_to_np_structured_array(df) #df_numerical_attrs.to_numpy() + dataset['shape'] = dataset['data'].shape + dataset['dtype'] = type(dataset['data']) + #dataset['data_units'] = file_obj['wave']['data_units'] + # + # Create attribute descriptions based on description_dict + dataset['attributes'] = {} + + # Annotate column headers if description_dict is non empty + if description_dict: + for column_name in df.columns: + column_attr_dict = description_dict['table_header'].get(column_name, + {'note':'there was no description available. Review instrument files.'}) + dataset['attributes'].update({column_name: utils.convert_attrdict_to_np_structured_array(column_attr_dict)}) + + #try: + # dataset['attributes'] = description_dict['table_header'].copy() + # for key in description_dict['table_header'].keys(): + # if not key in numerical_variables: + # dataset['attributes'].pop(key) # delete key + # else: + # dataset['attributes'][key] = utils.parse_attribute(dataset['attributes'][key]) + # if timestamps_name in categorical_variables: + # dataset['attributes'][timestamps_name] = utils.parse_attribute({'unit':'YYYY-MM-DD HH:MM:SS.ffffff'}) + #except ValueError as err: + # print(err) + + # Represent string values as fixed length strings in the HDF5 file, which need + # to be decoded as string when we read them. It provides better control than variable strings, + # at the expense of flexibility. + # https://docs.h5py.org/en/stable/strings.html + + + if table_preamble: + #header_dict["table_preamble"] = utils.convert_string_to_bytes(table_preamble) + tp_dataset = {} + tp_dataset['name'] = "table_preamble" + tp_dataset['data'] = utils.convert_string_to_bytes(table_preamble) + tp_dataset['shape'] = tp_dataset['data'].shape + tp_dataset['dtype'] = type(tp_dataset['data']) + tp_dataset['attributes'] = {} + file_dict['datasets'].append(tp_dataset) + + file_dict['datasets'].append(dataset) + + + #if categorical_variables: + # dataset = {} + # dataset['name'] = 'table_categorical_variables' + # dataset['data'] = dataframe_to_np_structured_array(df_categorical_attrs) #df_categorical_attrs.loc[:,categorical_variables].to_numpy() + # dataset['shape'] = dataset['data'].shape + # dataset['dtype'] = type(dataset['data']) + # if timestamps_name in categorical_variables: + # dataset['attributes'] = {timestamps_name: utils.parse_attribute({'unit':'YYYY-MM-DD HH:MM:SS.ffffff'})} + # file_dict['datasets'].append(dataset) + except: + return {} + return file_dict \ No newline at end of file diff --git a/instruments/readers/xps_ibw_reader.py b/instruments/readers/xps_ibw_reader.py index d73531e..b3881c6 100644 --- a/instruments/readers/xps_ibw_reader.py +++ b/instruments/readers/xps_ibw_reader.py @@ -1,79 +1,79 @@ -import os -from igor2.binarywave import load as loadibw - -def read_xps_ibw_file_as_dict(filename): - """ - Reads IBW files from the Multiphase Chemistry Group, which contain XPS spectra and acquisition settings, - and formats the data into a dictionary with the structure {datasets: list of datasets}. Each dataset in the - list has the following structure: - - { - 'name': 'name', - 'data': data_array, - 'data_units': 'units', - 'shape': data_shape, - 'dtype': data_type - } - - Parameters - ---------- - filename : str - The IBW filename from the Multiphase Chemistry Group beamline. - - Returns - ------- - file_dict : dict - A dictionary containing the datasets from the IBW file. - - Raises - ------ - ValueError - If the input IBW file is not a valid IBW file. - - """ - - - file_obj = loadibw(filename) - - required_keys = ['wData','data_units','dimension_units','note'] - if sum([item in required_keys for item in file_obj['wave'].keys()]) < len(required_keys): - raise ValueError('This is not a valid xps ibw file. It does not satisfy minimum adimissibility criteria.') - - file_dict = {} - path_tail, path_head = os.path.split(filename) - - # Group name and attributes - file_dict['name'] = path_head - file_dict['attributes_dict'] = {} - - # Convert notes of bytes class to string class and split string into a list of elements separated by '\r'. - notes_list = file_obj['wave']['note'].decode("utf-8").split('\r') - exclude_list = ['Excitation Energy'] - for item in notes_list: - if '=' in item: - key, value = tuple(item.split('=')) - # TODO: check if value can be converted into a numeric type. Now all values are string type - if not key in exclude_list: - file_dict['attributes_dict'][key] = value - - # TODO: talk to Thorsten to see if there is an easier way to access the below attributes - dimension_labels = file_obj['wave']['dimension_units'].decode("utf-8").split(']') - file_dict['attributes_dict']['dimension_units'] = [item+']' for item in dimension_labels[0:len(dimension_labels)-1]] - - # Datasets and their attributes - - file_dict['datasets'] = [] - - dataset = {} - dataset['name'] = 'spectrum' - dataset['data'] = file_obj['wave']['wData'] - dataset['data_units'] = file_obj['wave']['data_units'] - dataset['shape'] = dataset['data'].shape - dataset['dtype'] = type(dataset['data']) - - # TODO: include energy axis dataset - - file_dict['datasets'].append(dataset) - - +import os +from igor2.binarywave import load as loadibw + +def read_xps_ibw_file_as_dict(filename): + """ + Reads IBW files from the Multiphase Chemistry Group, which contain XPS spectra and acquisition settings, + and formats the data into a dictionary with the structure {datasets: list of datasets}. Each dataset in the + list has the following structure: + + { + 'name': 'name', + 'data': data_array, + 'data_units': 'units', + 'shape': data_shape, + 'dtype': data_type + } + + Parameters + ---------- + filename : str + The IBW filename from the Multiphase Chemistry Group beamline. + + Returns + ------- + file_dict : dict + A dictionary containing the datasets from the IBW file. + + Raises + ------ + ValueError + If the input IBW file is not a valid IBW file. + + """ + + + file_obj = loadibw(filename) + + required_keys = ['wData','data_units','dimension_units','note'] + if sum([item in required_keys for item in file_obj['wave'].keys()]) < len(required_keys): + raise ValueError('This is not a valid xps ibw file. It does not satisfy minimum adimissibility criteria.') + + file_dict = {} + path_tail, path_head = os.path.split(filename) + + # Group name and attributes + file_dict['name'] = path_head + file_dict['attributes_dict'] = {} + + # Convert notes of bytes class to string class and split string into a list of elements separated by '\r'. + notes_list = file_obj['wave']['note'].decode("utf-8").split('\r') + exclude_list = ['Excitation Energy'] + for item in notes_list: + if '=' in item: + key, value = tuple(item.split('=')) + # TODO: check if value can be converted into a numeric type. Now all values are string type + if not key in exclude_list: + file_dict['attributes_dict'][key] = value + + # TODO: talk to Thorsten to see if there is an easier way to access the below attributes + dimension_labels = file_obj['wave']['dimension_units'].decode("utf-8").split(']') + file_dict['attributes_dict']['dimension_units'] = [item+']' for item in dimension_labels[0:len(dimension_labels)-1]] + + # Datasets and their attributes + + file_dict['datasets'] = [] + + dataset = {} + dataset['name'] = 'spectrum' + dataset['data'] = file_obj['wave']['wData'] + dataset['data_units'] = file_obj['wave']['data_units'] + dataset['shape'] = dataset['data'].shape + dataset['dtype'] = type(dataset['data']) + + # TODO: include energy axis dataset + + file_dict['datasets'].append(dataset) + + return file_dict \ No newline at end of file diff --git a/notebooks/demo_create_and_visualize_hdf5_file.ipynb b/notebooks/demo_create_and_visualize_hdf5_file.ipynb index d88792f..93a4fee 100644 --- a/notebooks/demo_create_and_visualize_hdf5_file.ipynb +++ b/notebooks/demo_create_and_visualize_hdf5_file.ipynb @@ -1,151 +1,151 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "from nbutils import add_project_path_to_sys_path\n", - "\n", - "\n", - "# Add project root to sys.path\n", - "add_project_path_to_sys_path()\n", - "\n", - "import pandas as pd\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "\n", - "try:\n", - " import src.hdf5_writer as hdf5_writer\n", - " import src.hdf5_ops as hdf5_ops\n", - " import visualization.hdf5_vis as h5vis\n", - " import visualization.napp_plotlib as napp\n", - "\n", - " import utils.g5505_utils as utils\n", - " #import pipelines.metadata_revision as metadata_revision\n", - " print(\"Imports successful!\")\n", - "except ImportError as e:\n", - " print(f\"Import error: {e}\")\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Read the above specified input_file_path as a dataframe. \n", - "\n", - "Since we know this file was created from a Thorsten Table's format, we can use h5lib.read_mtable_as_dataframe() to read it.\n", - "\n", - "Then, we rename the 'name' column as 'filename', as this is the column's name use to idenfify files in subsequent functions.\n", - "Also, we augment the dataframe with a few categorical columns to be used as grouping variables when creating the hdf5 file's group hierarchy. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Define input file directory\n", - "\n", - "input_file_path = '../input_files/BeamTimeMetaData.h5'\n", - "output_dir_path = '../output_files'\n", - "if not os.path.exists(output_dir_path):\n", - " os.makedirs(output_dir_path)\n", - "\n", - "# Read BeamTimeMetaData.h5, containing Thorsten's Matlab Table\n", - "input_data_df = hdf5_ops.read_mtable_as_dataframe(input_file_path)\n", - "\n", - "# Preprocess Thorsten's input_data dataframe so that i can be used to create a newer .h5 file\n", - "# under certain grouping specificiations.\n", - "input_data_df = input_data_df.rename(columns = {'name':'filename'})\n", - "input_data_df = utils.augment_with_filenumber(input_data_df)\n", - "input_data_df = utils.augment_with_filetype(input_data_df)\n", - "input_data_df = utils.split_sample_col_into_sample_and_data_quality_cols(input_data_df)\n", - "input_data_df['lastModifiedDatestr'] = input_data_df['lastModifiedDatestr'].astype('datetime64[s]')\n", - "\n", - "input_data_df.columns\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We now create a hdf5 file with a 3-level group hierarchy based on the input_data and three grouping functions. Then\n", - "we visualize the group hierarchy of the created file as a treemap." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Define grouping functions to be passed into create_hdf5_file function. These can also be set\n", - "# as strings refering to categorical columns in input_data_df.\n", - "\n", - "test_grouping_funcs = True\n", - "if test_grouping_funcs:\n", - " group_by_sample = lambda x : utils.group_by_df_column(x,'sample')\n", - " group_by_type = lambda x : utils.group_by_df_column(x,'filetype')\n", - " group_by_filenumber = lambda x : utils.group_by_df_column(x,'filenumber')\n", - "else:\n", - " group_by_sample = 'sample'\n", - " group_by_type = 'filetype'\n", - " group_by_filenumber = 'filenumber'\n", - "\n", - "import pandas as pd\n", - "import h5py\n", - "\n", - "path_to_output_filename = os.path.normpath(os.path.join(output_dir_path, 'test.h5'))\n", - "\n", - "grouping_by_vars = ['sample', 'filenumber']\n", - "\n", - "path_to_output_filename = hdf5_writer.create_hdf5_file_from_dataframe(path_to_output_filename, \n", - " input_data_df, \n", - " grouping_by_vars\n", - " )\n", - "\n", - "annotation_dict = {'Campaign name': 'SLS-Campaign-2023',\n", - " 'Producers':'Thorsten, Luca, Zoe',\n", - " 'Startdate': str(input_data_df['lastModifiedDatestr'].min()),\n", - " 'Enddate': str(input_data_df['lastModifiedDatestr'].max())\n", - " }\n", - "\n", - "dataOpsObj = hdf5_ops.HDF5DataOpsManager(path_to_output_filename)\n", - "dataOpsObj.load_file_obj()\n", - "# Annotate root folder with annotation_dict\n", - "dataOpsObj.append_metadata('/',annotation_dict)\n", - "dataOpsObj.unload_file_obj()\n", - "\n", - "\n", - "\n", - "h5vis.display_group_hierarchy_on_a_treemap(path_to_output_filename)\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "multiphase_chemistry_env", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from nbutils import add_project_path_to_sys_path\n", + "\n", + "\n", + "# Add project root to sys.path\n", + "add_project_path_to_sys_path()\n", + "\n", + "import pandas as pd\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "try:\n", + " import src.hdf5_writer as hdf5_writer\n", + " import src.hdf5_ops as hdf5_ops\n", + " import visualization.hdf5_vis as h5vis\n", + " import visualization.napp_plotlib as napp\n", + "\n", + " import utils.g5505_utils as utils\n", + " #import pipelines.metadata_revision as metadata_revision\n", + " print(\"Imports successful!\")\n", + "except ImportError as e:\n", + " print(f\"Import error: {e}\")\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Read the above specified input_file_path as a dataframe. \n", + "\n", + "Since we know this file was created from a Thorsten Table's format, we can use h5lib.read_mtable_as_dataframe() to read it.\n", + "\n", + "Then, we rename the 'name' column as 'filename', as this is the column's name use to idenfify files in subsequent functions.\n", + "Also, we augment the dataframe with a few categorical columns to be used as grouping variables when creating the hdf5 file's group hierarchy. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Define input file directory\n", + "\n", + "input_file_path = '../input_files/BeamTimeMetaData.h5'\n", + "output_dir_path = '../output_files'\n", + "if not os.path.exists(output_dir_path):\n", + " os.makedirs(output_dir_path)\n", + "\n", + "# Read BeamTimeMetaData.h5, containing Thorsten's Matlab Table\n", + "input_data_df = hdf5_ops.read_mtable_as_dataframe(input_file_path)\n", + "\n", + "# Preprocess Thorsten's input_data dataframe so that i can be used to create a newer .h5 file\n", + "# under certain grouping specificiations.\n", + "input_data_df = input_data_df.rename(columns = {'name':'filename'})\n", + "input_data_df = utils.augment_with_filenumber(input_data_df)\n", + "input_data_df = utils.augment_with_filetype(input_data_df)\n", + "input_data_df = utils.split_sample_col_into_sample_and_data_quality_cols(input_data_df)\n", + "input_data_df['lastModifiedDatestr'] = input_data_df['lastModifiedDatestr'].astype('datetime64[s]')\n", + "\n", + "input_data_df.columns\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now create a hdf5 file with a 3-level group hierarchy based on the input_data and three grouping functions. Then\n", + "we visualize the group hierarchy of the created file as a treemap." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Define grouping functions to be passed into create_hdf5_file function. These can also be set\n", + "# as strings refering to categorical columns in input_data_df.\n", + "\n", + "test_grouping_funcs = True\n", + "if test_grouping_funcs:\n", + " group_by_sample = lambda x : utils.group_by_df_column(x,'sample')\n", + " group_by_type = lambda x : utils.group_by_df_column(x,'filetype')\n", + " group_by_filenumber = lambda x : utils.group_by_df_column(x,'filenumber')\n", + "else:\n", + " group_by_sample = 'sample'\n", + " group_by_type = 'filetype'\n", + " group_by_filenumber = 'filenumber'\n", + "\n", + "import pandas as pd\n", + "import h5py\n", + "\n", + "path_to_output_filename = os.path.normpath(os.path.join(output_dir_path, 'test.h5'))\n", + "\n", + "grouping_by_vars = ['sample', 'filenumber']\n", + "\n", + "path_to_output_filename = hdf5_writer.create_hdf5_file_from_dataframe(path_to_output_filename, \n", + " input_data_df, \n", + " grouping_by_vars\n", + " )\n", + "\n", + "annotation_dict = {'Campaign name': 'SLS-Campaign-2023',\n", + " 'Producers':'Thorsten, Luca, Zoe',\n", + " 'Startdate': str(input_data_df['lastModifiedDatestr'].min()),\n", + " 'Enddate': str(input_data_df['lastModifiedDatestr'].max())\n", + " }\n", + "\n", + "dataOpsObj = hdf5_ops.HDF5DataOpsManager(path_to_output_filename)\n", + "dataOpsObj.load_file_obj()\n", + "# Annotate root folder with annotation_dict\n", + "dataOpsObj.append_metadata('/',annotation_dict)\n", + "dataOpsObj.unload_file_obj()\n", + "\n", + "\n", + "\n", + "h5vis.display_group_hierarchy_on_a_treemap(path_to_output_filename)\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "multiphase_chemistry_env", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/demo_data_integration.ipynb b/notebooks/demo_data_integration.ipynb index e5e2a04..0a3a0c8 100644 --- a/notebooks/demo_data_integration.ipynb +++ b/notebooks/demo_data_integration.ipynb @@ -1,182 +1,182 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Data integration workflow of experimental campaign\n", - "\n", - "In this notebook, we will go through a our data integration workflow. This involves the following steps:\n", - "\n", - "1. Specify data integration file through YAML configuration file.\n", - "2. Create an integrated HDF5 file of experimental campaign from configuration file.\n", - "3. Display the created HDF5 file using a treemap\n", - "\n", - "## Import libraries and modules\n", - "\n", - "* Excecute (or Run) the Cell below" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from nbutils import add_project_path_to_sys_path\n", - "\n", - "# Add project root to sys.path\n", - "add_project_path_to_sys_path()\n", - "\n", - "try:\n", - " import visualization.hdf5_vis as hdf5_vis\n", - " import pipelines.data_integration as data_integration\n", - " print(\"Imports successful!\")\n", - "except ImportError as e:\n", - " print(f\"Import error: {e}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 1: Specify data integration task through YAML configuration file\n", - "\n", - "* Create your configuration file (i.e., *.yaml file) adhering to the example yaml file in the input folder.\n", - "* Set up input directory and output directory paths and Excecute Cell.\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "#output_filename_path = 'output_files/unified_file_smog_chamber_2024-04-07_UTC-OFST_+0200_NG.h5'\n", - "yaml_config_file_path = '../input_files/data_integr_config_file_TBR.yaml'\n", - "\n", - "#path_to_input_directory = 'output_files/kinetic_flowtube_study_2022-01-31_LuciaI'\n", - "#path_to_hdf5_file = hdf5_lib.create_hdf5_file_from_filesystem_path(path_to_input_directory)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 2: Create an integrated HDF5 file of experimental campaign.\n", - "\n", - "* Excecute Cell. Here we run the function `integrate_data_sources` with input argument as the previously specified YAML config file." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "hdf5_file_path = data_integration.run_pipeline(yaml_config_file_path)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "hdf5_file_path " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Display integrated HDF5 file using a treemap\n", - "\n", - "* Excecute Cell. A visual representation in html format of the integrated file should be displayed and stored in the output directory folder" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "if isinstance(hdf5_file_path ,list):\n", - " for path_item in hdf5_file_path :\n", - " hdf5_vis.display_group_hierarchy_on_a_treemap(path_item)\n", - "else:\n", - " hdf5_vis.display_group_hierarchy_on_a_treemap(hdf5_file_path)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import src.hdf5_ops as h5de \n", - "h5de.serialize_metadata(hdf5_file_path[0],folder_depth=3,output_format='yaml')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import src.hdf5_ops as h5de \n", - "print(hdf5_file_path)\n", - "DataOpsAPI = h5de.HDF5DataOpsManager(hdf5_file_path[0])\n", - "\n", - "DataOpsAPI.load_file_obj()\n", - "\n", - "#DataOpsAPI.reformat_datetime_column('ICAD/HONO/2022_11_22_Channel1_Data.dat/data_table',\n", - "# 'Start Date/Time (UTC)',\n", - "# '%Y-%m-%d %H:%M:%S.%f', '%Y-%m-%d %H:%M:%S')\n", - "DataOpsAPI.extract_and_load_dataset_metadata()\n", - "df = DataOpsAPI.dataset_metadata_df\n", - "print(df.head())\n", - "\n", - "DataOpsAPI.unload_file_obj()\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [], - "source": [ - "DataOpsAPI.load_file_obj()\n", - "\n", - "DataOpsAPI.append_metadata('/',{'test_attr':'this is a test value'})\n", - "\n", - "DataOpsAPI.unload_file_obj()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "multiphase_chemistry_env", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Data integration workflow of experimental campaign\n", + "\n", + "In this notebook, we will go through a our data integration workflow. This involves the following steps:\n", + "\n", + "1. Specify data integration file through YAML configuration file.\n", + "2. Create an integrated HDF5 file of experimental campaign from configuration file.\n", + "3. Display the created HDF5 file using a treemap\n", + "\n", + "## Import libraries and modules\n", + "\n", + "* Excecute (or Run) the Cell below" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from nbutils import add_project_path_to_sys_path\n", + "\n", + "# Add project root to sys.path\n", + "add_project_path_to_sys_path()\n", + "\n", + "try:\n", + " import visualization.hdf5_vis as hdf5_vis\n", + " import pipelines.data_integration as data_integration\n", + " print(\"Imports successful!\")\n", + "except ImportError as e:\n", + " print(f\"Import error: {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 1: Specify data integration task through YAML configuration file\n", + "\n", + "* Create your configuration file (i.e., *.yaml file) adhering to the example yaml file in the input folder.\n", + "* Set up input directory and output directory paths and Excecute Cell.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "#output_filename_path = 'output_files/unified_file_smog_chamber_2024-04-07_UTC-OFST_+0200_NG.h5'\n", + "yaml_config_file_path = '../input_files/data_integr_config_file_TBR.yaml'\n", + "\n", + "#path_to_input_directory = 'output_files/kinetic_flowtube_study_2022-01-31_LuciaI'\n", + "#path_to_hdf5_file = hdf5_lib.create_hdf5_file_from_filesystem_path(path_to_input_directory)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 2: Create an integrated HDF5 file of experimental campaign.\n", + "\n", + "* Excecute Cell. Here we run the function `integrate_data_sources` with input argument as the previously specified YAML config file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "hdf5_file_path = data_integration.run_pipeline(yaml_config_file_path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "hdf5_file_path " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Display integrated HDF5 file using a treemap\n", + "\n", + "* Excecute Cell. A visual representation in html format of the integrated file should be displayed and stored in the output directory folder" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "if isinstance(hdf5_file_path ,list):\n", + " for path_item in hdf5_file_path :\n", + " hdf5_vis.display_group_hierarchy_on_a_treemap(path_item)\n", + "else:\n", + " hdf5_vis.display_group_hierarchy_on_a_treemap(hdf5_file_path)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import src.hdf5_ops as h5de \n", + "h5de.serialize_metadata(hdf5_file_path[0],folder_depth=3,output_format='yaml')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import src.hdf5_ops as h5de \n", + "print(hdf5_file_path)\n", + "DataOpsAPI = h5de.HDF5DataOpsManager(hdf5_file_path[0])\n", + "\n", + "DataOpsAPI.load_file_obj()\n", + "\n", + "#DataOpsAPI.reformat_datetime_column('ICAD/HONO/2022_11_22_Channel1_Data.dat/data_table',\n", + "# 'Start Date/Time (UTC)',\n", + "# '%Y-%m-%d %H:%M:%S.%f', '%Y-%m-%d %H:%M:%S')\n", + "DataOpsAPI.extract_and_load_dataset_metadata()\n", + "df = DataOpsAPI.dataset_metadata_df\n", + "print(df.head())\n", + "\n", + "DataOpsAPI.unload_file_obj()\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "DataOpsAPI.load_file_obj()\n", + "\n", + "DataOpsAPI.append_metadata('/',{'test_attr':'this is a test value'})\n", + "\n", + "DataOpsAPI.unload_file_obj()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "multiphase_chemistry_env", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/notebooks/demo_h5_file_2_obis_props_mapping.py b/notebooks/demo_h5_file_2_obis_props_mapping.py index f859f26..8b4ab38 100644 --- a/notebooks/demo_h5_file_2_obis_props_mapping.py +++ b/notebooks/demo_h5_file_2_obis_props_mapping.py @@ -1,79 +1,79 @@ -import os -from nbutils import add_project_path_to_sys_path - - -# Add project root to sys.path -add_project_path_to_sys_path() - -import datetime -import logging - -try: - import src.openbis_lib as openbis_lib - import src.hdf5_ops as hdf5_ops - #import pipelines.metadata_revision as metadata_revision - print("Imports successful!") -except ImportError as e: - print(f"Import error: {e}") - -def main(): - - #df_h5 = hdf5_lib.read_hdf5_as_dataframe_v2('BeamTimeMetaData.h5') - #df_h5['lastModifiedDatestr'] = df_h5['lastModifiedDatestr'].astype('datetime64[ns]') - #df_h5 = df_h5.sort_values(by='lastModifiedDatestr') - - - openbis_obj = openbis_lib.initialize_openbis_obj() - - # Create df with sample measurements of type 'ISS_MEASUREMENT' - samples = openbis_obj.get_samples(type='ISS_MEASUREMENT',props=['FILENUMBER']) - for sample in samples: - print(type(sample)) - print(sample.identifier) - df_openbis = samples.df.copy(deep=True) - h5_file_path = os.path.join(os.path.curdir,'input_files\\BeamTimeMetaData.h5') - - df_h5 = hdf5_ops.read_mtable_as_dataframe(h5_file_path) - - # dataframe preprocessing steps - df_h5, df_openbis = openbis_lib.align_datetime_observation_windows(df_h5, df_openbis) - df_openbis = openbis_lib.pair_openbis_and_h5_dataframes(df_openbis, df_h5, 'REFORMATED_FILENUMBER', 'name') - - - - current_date = datetime.date.today() - log_filename = 'logs\\computed_openbis_props_logs_' + current_date.strftime('%d-%m-%Y') + '.log' - logging_flag = True - - #logger = logging.getLogger(__name__) - #logger.setLevel(logging.DEBUG) - - log_file_path = os.path.join(os.path.curdir,log_filename) - - logging.basicConfig(filename=log_file_path, - level=logging.DEBUG, - format="%(asctime)s %(levelname)s %(message)s", - datefmt="%d-%m-%Y %H:%M:%S", - ) - - for sample_idx in df_openbis.index: - - # logging.basicConfig(log_filename) - #print(formatted_dict) - sample_props_dict = openbis_lib.compute_openbis_sample_props_from_h5(df_openbis, df_h5, sample_idx) - - formatted_dict = [f"{key}:{value}" for key, value in sample_props_dict.items()] - formatted_dict = "\n".join(formatted_dict) - - logging.debug('\n'+formatted_dict) - - - #print(props_dict) - openbis_obj.logout() - - # Choose samples and specifici properties to update: create a log - - -if __name__=="__main__": - main() - +import os +from nbutils import add_project_path_to_sys_path + + +# Add project root to sys.path +add_project_path_to_sys_path() + +import datetime +import logging + +try: + import src.openbis_lib as openbis_lib + import src.hdf5_ops as hdf5_ops + #import pipelines.metadata_revision as metadata_revision + print("Imports successful!") +except ImportError as e: + print(f"Import error: {e}") + +def main(): + + #df_h5 = hdf5_lib.read_hdf5_as_dataframe_v2('BeamTimeMetaData.h5') + #df_h5['lastModifiedDatestr'] = df_h5['lastModifiedDatestr'].astype('datetime64[ns]') + #df_h5 = df_h5.sort_values(by='lastModifiedDatestr') + + + openbis_obj = openbis_lib.initialize_openbis_obj() + + # Create df with sample measurements of type 'ISS_MEASUREMENT' + samples = openbis_obj.get_samples(type='ISS_MEASUREMENT',props=['FILENUMBER']) + for sample in samples: + print(type(sample)) + print(sample.identifier) + df_openbis = samples.df.copy(deep=True) + h5_file_path = os.path.join(os.path.curdir,'input_files\\BeamTimeMetaData.h5') + + df_h5 = hdf5_ops.read_mtable_as_dataframe(h5_file_path) + + # dataframe preprocessing steps + df_h5, df_openbis = openbis_lib.align_datetime_observation_windows(df_h5, df_openbis) + df_openbis = openbis_lib.pair_openbis_and_h5_dataframes(df_openbis, df_h5, 'REFORMATED_FILENUMBER', 'name') + + + + current_date = datetime.date.today() + log_filename = 'logs\\computed_openbis_props_logs_' + current_date.strftime('%d-%m-%Y') + '.log' + logging_flag = True + + #logger = logging.getLogger(__name__) + #logger.setLevel(logging.DEBUG) + + log_file_path = os.path.join(os.path.curdir,log_filename) + + logging.basicConfig(filename=log_file_path, + level=logging.DEBUG, + format="%(asctime)s %(levelname)s %(message)s", + datefmt="%d-%m-%Y %H:%M:%S", + ) + + for sample_idx in df_openbis.index: + + # logging.basicConfig(log_filename) + #print(formatted_dict) + sample_props_dict = openbis_lib.compute_openbis_sample_props_from_h5(df_openbis, df_h5, sample_idx) + + formatted_dict = [f"{key}:{value}" for key, value in sample_props_dict.items()] + formatted_dict = "\n".join(formatted_dict) + + logging.debug('\n'+formatted_dict) + + + #print(props_dict) + openbis_obj.logout() + + # Choose samples and specifici properties to update: create a log + + +if __name__=="__main__": + main() + diff --git a/notebooks/demo_hdf5_data_sharing_and_plotting.ipynb b/notebooks/demo_hdf5_data_sharing_and_plotting.ipynb index 0193449..c1ce322 100644 --- a/notebooks/demo_hdf5_data_sharing_and_plotting.ipynb +++ b/notebooks/demo_hdf5_data_sharing_and_plotting.ipynb @@ -1,96 +1,96 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "from nbutils import add_project_path_to_sys_path\n", - "\n", - "\n", - "# Add project root to sys.path\n", - "add_project_path_to_sys_path()\n", - "\n", - "import pandas as pd\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "\n", - "try:\n", - " import src.hdf5_ops as hdf5_ops\n", - " import visualization.napp_plotlib as napp\n", - " #import pipelines.metadata_revision as metadata_revision\n", - " print(\"Imports successful!\")\n", - "except ImportError as e:\n", - " print(f\"Import error: {e}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Define h5 file name and make sure file is located at the current working dir\n", - "filename = '../input_files/FileList_v2.h5'\n", - "\n", - "# Read h5 file into dataframe\n", - "dataframe = hdf5_ops.read_mtable_as_dataframe(filename)\n", - "\n", - "\n", - "dataframe['lastModifiedDatestr']\n", - "print(dataframe.columns)\n", - "\n", - "dataframe.head()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "dataframe['image'][0].shape\n", - "\n", - "name_filter = (dataframe['name'] == '0116116_Cl2p_750eV.ibw').to_numpy()\n", - "date_filter = np.array(['Jun-2023' in date for date in dataframe['lastModifiedDatestr']])\n", - "\n", - "filter = np.logical_and(name_filter.flatten(),date_filter.flatten()) \n", - "\n", - "napp.plot_image(dataframe,filter)\n", - "napp.plot_spectra(dataframe,filter)\n", - "\n", - "name_filter = np.array(['merge' in name for name in dataframe['name'] ])\n", - "date_filter = np.array(['Jun-2023' in date for date in dataframe['lastModifiedDatestr']])\n", - "filter = np.logical_and(name_filter.flatten(),date_filter.flatten()) \n", - "\n", - "\n", - "napp.plot_spectra(dataframe,filter)\n", - "\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "multiphase_chemistry_env", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" - }, - "orig_nbformat": 4 - }, - "nbformat": 4, - "nbformat_minor": 2 -} +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from nbutils import add_project_path_to_sys_path\n", + "\n", + "\n", + "# Add project root to sys.path\n", + "add_project_path_to_sys_path()\n", + "\n", + "import pandas as pd\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "try:\n", + " import src.hdf5_ops as hdf5_ops\n", + " import visualization.napp_plotlib as napp\n", + " #import pipelines.metadata_revision as metadata_revision\n", + " print(\"Imports successful!\")\n", + "except ImportError as e:\n", + " print(f\"Import error: {e}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Define h5 file name and make sure file is located at the current working dir\n", + "filename = '../input_files/FileList_v2.h5'\n", + "\n", + "# Read h5 file into dataframe\n", + "dataframe = hdf5_ops.read_mtable_as_dataframe(filename)\n", + "\n", + "\n", + "dataframe['lastModifiedDatestr']\n", + "print(dataframe.columns)\n", + "\n", + "dataframe.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "dataframe['image'][0].shape\n", + "\n", + "name_filter = (dataframe['name'] == '0116116_Cl2p_750eV.ibw').to_numpy()\n", + "date_filter = np.array(['Jun-2023' in date for date in dataframe['lastModifiedDatestr']])\n", + "\n", + "filter = np.logical_and(name_filter.flatten(),date_filter.flatten()) \n", + "\n", + "napp.plot_image(dataframe,filter)\n", + "napp.plot_spectra(dataframe,filter)\n", + "\n", + "name_filter = np.array(['merge' in name for name in dataframe['name'] ])\n", + "date_filter = np.array(['Jun-2023' in date for date in dataframe['lastModifiedDatestr']])\n", + "filter = np.logical_and(name_filter.flatten(),date_filter.flatten()) \n", + "\n", + "\n", + "napp.plot_spectra(dataframe,filter)\n", + "\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "multiphase_chemistry_env", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/demo_single_sample_update_to_openbis.py b/notebooks/demo_single_sample_update_to_openbis.py index 65d0bb3..0647915 100644 --- a/notebooks/demo_single_sample_update_to_openbis.py +++ b/notebooks/demo_single_sample_update_to_openbis.py @@ -1,98 +1,98 @@ -import os -from nbutils import add_project_path_to_sys_path - - -# Add project root to sys.path -add_project_path_to_sys_path() - -import datetime -import logging - -try: - import src.openbis_lib as openbis_lib - import src.hdf5_ops as hdf5_ops - #import pipelines.metadata_revision as metadata_revision - print("Imports successful!") -except ImportError as e: - print(f"Import error: {e}") - - -def main(): - - #df_h5 = hdf5_lib.read_hdf5_as_dataframe_v2('BeamTimeMetaData.h5') - #df_h5['lastModifiedDatestr'] = df_h5['lastModifiedDatestr'].astype('datetime64[ns]') - #df_h5 = df_h5.sort_values(by='lastModifiedDatestr') - - - openbis_obj = openbis_lib.initialize_openbis_obj() - - # Create df with sample measurements of type 'ISS_MEASUREMENT' - samples = openbis_obj.get_samples(type='ISS_MEASUREMENT',props=['FILENUMBER']) - for sample in samples: - print(type(sample)) - print(sample.identifier) - df_openbis = samples.df.copy(deep=True) - h5_file_path = os.path.join(os.path.curdir,'input_files\\BeamTimeMetaData.h5') - df_h5 = hdf5_ops.read_mtable_as_dataframe(h5_file_path) - - # dataframe preprocessing steps - df_h5, df_openbis = openbis_lib.align_datetime_observation_windows(df_h5, df_openbis) - df_openbis = openbis_lib.pair_openbis_and_h5_dataframes(df_openbis, df_h5, 'REFORMATED_FILENUMBER', 'name') - - - - current_date = datetime.date.today() - log_filename = 'logs\\computed_openbis_props_logs_' + current_date.strftime('%d-%m-%Y') + '.log' - logging_flag = True - - #logger = logging.getLogger(__name__) - #logger.setLevel(logging.DEBUG) - - log_file_path = os.path.join(os.path.curdir,log_filename) - - logging.basicConfig(filename=log_file_path, - level=logging.DEBUG, - format="%(asctime)s %(levelname)s %(message)s", - datefmt="%d-%m-%Y %H:%M:%S", - ) - - # update sample properties in openbis database only if they are labeled as bad - - props_include_list = ['sample_name', 'temp', 'cell_pressure','method_name', 'region', 'lens_mode', 'acq_mode', 'dwell_time'] - props_include_list = ['ke_range_center','ke_range_step'] - props_include_list = [ 'temp', 'cell_pressure','photon_energy','dwell_time','passenergy','ke_range_center','ke_step_center','position_x','position_y','position_z'] - - props_include_list = ['position_x','position_y','position_z'] - props_include_list = [ 'temp', 'cell_pressure','photon_energy','dwell_time','passenergy','ke_range_center','ke_step_center'] - - - for sample_idx in df_openbis.index: - - # logging.basicConfig(log_filename) - #print(formatted_dict) - sample_props_dict = openbis_lib.compute_openbis_sample_props_from_h5(df_openbis, df_h5, sample_idx) - - #sample_props_dict[ke_range_center] - - formatted_dict = [f"{key}:{value}" for key, value in sample_props_dict.items()] - formatted_dict = "\n".join(formatted_dict) - logging.debug('\n'+formatted_dict) - try: - filenumber = -1 if sample_props_dict['FILENUMBER'] == '' else int(sample_props_dict['FILENUMBER']) - - if filenumber >= 85 : - print(filenumber) - #if 'bad' in sample_props_dict['sample_name']: - logging.info('The above sample is to be updated in openbis:') - openbis_lib.single_sample_update(sample_props_dict,samples,props_include_list) - except KeyError: - logging.error(KeyError) - #print(props_dict) - openbis_obj.logout() - - # Choose samples and specifici properties to update: create a log - - -if __name__=="__main__": - main() - +import os +from nbutils import add_project_path_to_sys_path + + +# Add project root to sys.path +add_project_path_to_sys_path() + +import datetime +import logging + +try: + import src.openbis_lib as openbis_lib + import src.hdf5_ops as hdf5_ops + #import pipelines.metadata_revision as metadata_revision + print("Imports successful!") +except ImportError as e: + print(f"Import error: {e}") + + +def main(): + + #df_h5 = hdf5_lib.read_hdf5_as_dataframe_v2('BeamTimeMetaData.h5') + #df_h5['lastModifiedDatestr'] = df_h5['lastModifiedDatestr'].astype('datetime64[ns]') + #df_h5 = df_h5.sort_values(by='lastModifiedDatestr') + + + openbis_obj = openbis_lib.initialize_openbis_obj() + + # Create df with sample measurements of type 'ISS_MEASUREMENT' + samples = openbis_obj.get_samples(type='ISS_MEASUREMENT',props=['FILENUMBER']) + for sample in samples: + print(type(sample)) + print(sample.identifier) + df_openbis = samples.df.copy(deep=True) + h5_file_path = os.path.join(os.path.curdir,'input_files\\BeamTimeMetaData.h5') + df_h5 = hdf5_ops.read_mtable_as_dataframe(h5_file_path) + + # dataframe preprocessing steps + df_h5, df_openbis = openbis_lib.align_datetime_observation_windows(df_h5, df_openbis) + df_openbis = openbis_lib.pair_openbis_and_h5_dataframes(df_openbis, df_h5, 'REFORMATED_FILENUMBER', 'name') + + + + current_date = datetime.date.today() + log_filename = 'logs\\computed_openbis_props_logs_' + current_date.strftime('%d-%m-%Y') + '.log' + logging_flag = True + + #logger = logging.getLogger(__name__) + #logger.setLevel(logging.DEBUG) + + log_file_path = os.path.join(os.path.curdir,log_filename) + + logging.basicConfig(filename=log_file_path, + level=logging.DEBUG, + format="%(asctime)s %(levelname)s %(message)s", + datefmt="%d-%m-%Y %H:%M:%S", + ) + + # update sample properties in openbis database only if they are labeled as bad + + props_include_list = ['sample_name', 'temp', 'cell_pressure','method_name', 'region', 'lens_mode', 'acq_mode', 'dwell_time'] + props_include_list = ['ke_range_center','ke_range_step'] + props_include_list = [ 'temp', 'cell_pressure','photon_energy','dwell_time','passenergy','ke_range_center','ke_step_center','position_x','position_y','position_z'] + + props_include_list = ['position_x','position_y','position_z'] + props_include_list = [ 'temp', 'cell_pressure','photon_energy','dwell_time','passenergy','ke_range_center','ke_step_center'] + + + for sample_idx in df_openbis.index: + + # logging.basicConfig(log_filename) + #print(formatted_dict) + sample_props_dict = openbis_lib.compute_openbis_sample_props_from_h5(df_openbis, df_h5, sample_idx) + + #sample_props_dict[ke_range_center] + + formatted_dict = [f"{key}:{value}" for key, value in sample_props_dict.items()] + formatted_dict = "\n".join(formatted_dict) + logging.debug('\n'+formatted_dict) + try: + filenumber = -1 if sample_props_dict['FILENUMBER'] == '' else int(sample_props_dict['FILENUMBER']) + + if filenumber >= 85 : + print(filenumber) + #if 'bad' in sample_props_dict['sample_name']: + logging.info('The above sample is to be updated in openbis:') + openbis_lib.single_sample_update(sample_props_dict,samples,props_include_list) + except KeyError: + logging.error(KeyError) + #print(props_dict) + openbis_obj.logout() + + # Choose samples and specifici properties to update: create a log + + +if __name__=="__main__": + main() + diff --git a/notebooks/example_workflow_metadata_annotation.ipynb b/notebooks/example_workflow_metadata_annotation.ipynb index fc8b4bf..86dd99c 100644 --- a/notebooks/example_workflow_metadata_annotation.ipynb +++ b/notebooks/example_workflow_metadata_annotation.ipynb @@ -1,172 +1,172 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Metadata Annotation Process\n", - "\n", - "In this notebook, we will go through a simple metadata annotation process. This involves the following steps:\n", - "\n", - "1. Define an HDF5 file.\n", - "2. Create a YAML representation of the HDF5 file.\n", - "3. Edit and augment the YAML with metadata.\n", - "4. Update the original file based on the edited YAML.\n", - "\n", - "\n", - "## Import libraries and modules\n", - "\n", - "* Excecute (or Run) the Cell below" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Imports successful!\n" - ] - } - ], - "source": [ - "import os\n", - "from nbutils import add_project_path_to_sys_path\n", - "\n", - "\n", - "# Add project root to sys.path\n", - "add_project_path_to_sys_path()\n", - "\n", - "try:\n", - " import src.hdf5_ops as hdf5_ops\n", - " import pipelines.metadata_revision as metadata_revision\n", - " print(\"Imports successful!\")\n", - "except ImportError as e:\n", - " print(f\"Import error: {e}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 1: Define an HDF5 file\n", - "\n", - "* Set up the string variable `hdf5_file_path` with the path to the HDF5 file of interest.\n", - "* Excecute Cell." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "hdf5_file_path = \"../output_files/collection_kinetic_flowtube_study_LuciaI_2022-01-31_2023-06-29/kinetic_flowtube_study_LuciaI_2023-06-29.h5\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 2: Create a YAML Representation of the File\n", - "\n", - "We now convert HDF5 file structure and existing metadata into a YAML format. This will be used to add and edit metadata attributes.\n", - "\n", - "* Excecute Cell." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The YAML file representation output_files/collection_kinetic_flowtube_study_LuciaI_2022-01-31_2023-06-29/kinetic_flowtube_study_LuciaI_2023-06-29.json of the HDF5 file output_files/collection_kinetic_flowtube_study_LuciaI_2022-01-31_2023-06-29/kinetic_flowtube_study_LuciaI_2023-06-29.h5 was created successfully.\n" - ] - } - ], - "source": [ - "yaml_file_path = hdf5_ops.serialize_metadata(hdf5_file_path,output_format='json')\n", - "\n", - "if os.path.exists(yaml_file_path):\n", - " print(f'The YAML file representation {yaml_file_path} of the HDF5 file {hdf5_file_path} was created successfully.')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 3: Edit and Augment YAML with Metadata\n", - "\n", - "We can now manually edit the YAML file to add metadata.\n", - "* (Optional) automate your metadata annotation process by creating a program that takes the YAMl file and returns the modified version of it.\n", - "* Excecute Cell." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def metadata_annotation_process(yaml_file_path):\n", - "\n", - " # Include metadata annotation logic, e.g., load yaml file and modify its content accordingly\n", - "\n", - " print(f'Ensure your edits to {yaml_file_path} have been properly incorporated and saved.')\n", - "\n", - " return yaml_file_path\n", - "\n", - "yaml_file_path = metadata_annotation_process(yaml_file_path)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Step 4: Update the Original File Based on the Edited YAML\n", - "\n", - "Lastly, we will update the original file with the metadata from the YAML file.\n", - "\n", - "* Excecute Cell." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "metadata_revision.update_hdf5_file_with_review(hdf5_file_path,yaml_file_path)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "multiphase_chemistry_env", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.9" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Metadata Annotation Process\n", + "\n", + "In this notebook, we will go through a simple metadata annotation process. This involves the following steps:\n", + "\n", + "1. Define an HDF5 file.\n", + "2. Create a YAML representation of the HDF5 file.\n", + "3. Edit and augment the YAML with metadata.\n", + "4. Update the original file based on the edited YAML.\n", + "\n", + "\n", + "## Import libraries and modules\n", + "\n", + "* Excecute (or Run) the Cell below" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Imports successful!\n" + ] + } + ], + "source": [ + "import os\n", + "from nbutils import add_project_path_to_sys_path\n", + "\n", + "\n", + "# Add project root to sys.path\n", + "add_project_path_to_sys_path()\n", + "\n", + "try:\n", + " import src.hdf5_ops as hdf5_ops\n", + " import pipelines.metadata_revision as metadata_revision\n", + " print(\"Imports successful!\")\n", + "except ImportError as e:\n", + " print(f\"Import error: {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 1: Define an HDF5 file\n", + "\n", + "* Set up the string variable `hdf5_file_path` with the path to the HDF5 file of interest.\n", + "* Excecute Cell." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "hdf5_file_path = \"../output_files/collection_kinetic_flowtube_study_LuciaI_2022-01-31_2023-06-29/kinetic_flowtube_study_LuciaI_2023-06-29.h5\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 2: Create a YAML Representation of the File\n", + "\n", + "We now convert HDF5 file structure and existing metadata into a YAML format. This will be used to add and edit metadata attributes.\n", + "\n", + "* Excecute Cell." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The YAML file representation output_files/collection_kinetic_flowtube_study_LuciaI_2022-01-31_2023-06-29/kinetic_flowtube_study_LuciaI_2023-06-29.json of the HDF5 file output_files/collection_kinetic_flowtube_study_LuciaI_2022-01-31_2023-06-29/kinetic_flowtube_study_LuciaI_2023-06-29.h5 was created successfully.\n" + ] + } + ], + "source": [ + "yaml_file_path = hdf5_ops.serialize_metadata(hdf5_file_path,output_format='json')\n", + "\n", + "if os.path.exists(yaml_file_path):\n", + " print(f'The YAML file representation {yaml_file_path} of the HDF5 file {hdf5_file_path} was created successfully.')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 3: Edit and Augment YAML with Metadata\n", + "\n", + "We can now manually edit the YAML file to add metadata.\n", + "* (Optional) automate your metadata annotation process by creating a program that takes the YAMl file and returns the modified version of it.\n", + "* Excecute Cell." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def metadata_annotation_process(yaml_file_path):\n", + "\n", + " # Include metadata annotation logic, e.g., load yaml file and modify its content accordingly\n", + "\n", + " print(f'Ensure your edits to {yaml_file_path} have been properly incorporated and saved.')\n", + "\n", + " return yaml_file_path\n", + "\n", + "yaml_file_path = metadata_annotation_process(yaml_file_path)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 4: Update the Original File Based on the Edited YAML\n", + "\n", + "Lastly, we will update the original file with the metadata from the YAML file.\n", + "\n", + "* Excecute Cell." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "metadata_revision.update_hdf5_file_with_review(hdf5_file_path,yaml_file_path)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "multiphase_chemistry_env", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/nbutils.py b/notebooks/nbutils.py index cf0bfa3..ada4faa 100644 --- a/notebooks/nbutils.py +++ b/notebooks/nbutils.py @@ -1,16 +1,16 @@ -import sys -import os - -def add_project_path_to_sys_path(): - """ - Adds the project path (root directory containing the package) to sys.path. - """ - # Determine the root directory (project_root, which contains 'dima') - notebook_dir = os.getcwd() # Current working directory (assumes running from notebooks/) - project_path = os.path.normpath(os.path.join(notebook_dir, "..")) # Move up to project root - - if project_path not in sys.path: # Avoid duplicate entries - sys.path.append(project_path) - -if __name__ == "__main__": +import sys +import os + +def add_project_path_to_sys_path(): + """ + Adds the project path (root directory containing the package) to sys.path. + """ + # Determine the root directory (project_root, which contains 'dima') + notebook_dir = os.getcwd() # Current working directory (assumes running from notebooks/) + project_path = os.path.normpath(os.path.join(notebook_dir, "..")) # Move up to project root + + if project_path not in sys.path: # Avoid duplicate entries + sys.path.append(project_path) + +if __name__ == "__main__": add_project_path_to_sys_path() \ No newline at end of file diff --git a/pipelines/data_integration.py b/pipelines/data_integration.py index 46a7e36..cd453c0 100644 --- a/pipelines/data_integration.py +++ b/pipelines/data_integration.py @@ -1,265 +1,265 @@ -import sys -import os -import re - -try: - thisFilePath = os.path.abspath(__file__) -except NameError: - print("Error: __file__ is not available. Ensure the script is being run from a file.") - print("[Notice] Path to DIMA package may not be resolved properly.") - thisFilePath = os.getcwd() # Use current directory or specify a default - -dimaPath = os.path.normpath(os.path.join(thisFilePath, "..",'..')) # Move up to project root - -if dimaPath not in sys.path: # Avoid duplicate entries - sys.path.append(dimaPath) - - -import yaml -import logging -from datetime import datetime -# Importing chain class from itertools -from itertools import chain - -# Import DIMA modules -import src.hdf5_writer as hdf5_lib -import utils.g5505_utils as utils -from instruments.readers import filereader_registry - -allowed_file_extensions = filereader_registry.file_extensions - -def _generate_datetime_dict(datetime_steps): - """ Generate the datetime augment dictionary from datetime steps. """ - datetime_augment_dict = {} - for datetime_step in datetime_steps: - #tmp = datetime.strptime(datetime_step, '%Y-%m-%d %H-%M-%S') - datetime_augment_dict[datetime_step] = [ - datetime_step.strftime('%Y-%m-%d'), datetime_step.strftime('%Y_%m_%d'), datetime_step.strftime('%Y.%m.%d'), datetime_step.strftime('%Y%m%d') - ] - return datetime_augment_dict - -def load_config_and_setup_logging(yaml_config_file_path, log_dir): - """Load YAML configuration file, set up logging, and validate required keys and datetime_steps.""" - - # Define required keys - required_keys = [ - 'experiment', 'contact', 'input_file_directory', 'output_file_directory', - 'instrument_datafolder', 'project', 'actris_level' - ] - - # Supported integration modes - supported_integration_modes = ['collection', 'single_experiment'] - - - # Set up logging - date = utils.created_at("%Y_%m").replace(":", "-") - utils.setup_logging(log_dir, f"integrate_data_sources_{date}.log") - - # Load YAML configuration file - with open(yaml_config_file_path, 'r') as stream: - try: - config_dict = yaml.load(stream, Loader=yaml.FullLoader) - except yaml.YAMLError as exc: - logging.error("Error loading YAML file: %s", exc) - raise ValueError(f"Failed to load YAML file: {exc}") - - # Check if required keys are present - missing_keys = [key for key in required_keys if key not in config_dict] - if missing_keys: - raise KeyError(f"Missing required keys in YAML configuration: {missing_keys}") - - # Check the instrument_datafolder required type and ensure the list is of at least length one. - if isinstance(config_dict['instrument_datafolder'], list) and not len(config_dict['instrument_datafolder'])>=1: - raise ValueError('Invalid value for key "instrument_datafolder". Expected a list of strings with at least one item.' - 'Each item represents a subfolder name in the input file directory, where the name' - 'must match the format "[/]".' - 'The first subfolder name is required, and the second is optional. ' - 'Examples of valid values: "level1", "level1/level2".') - - # Define the pattern for valid subfolder names: `subfolder` or `subfolder/subfolder` - #valid_pattern = re.compile(r'^[^/]+(/[^/]+)?$') - - # Validate each subfolder name - #for folder in config_dict['instrument_folder']: - # if not isinstance(folder, str) or not valid_pattern.match(folder): - # raise ValueError( - # 'Invalid value for key "instrument_folder" in YAML file.' - # 'Each item must be a string matching the format ' - # '"[/]". The first subfolder name is required, and the second is optional. ' - # 'Examples of valid values: "level1", "level1/level2". ' - # f'Invalid item: {folder}' - # ) - - # Validate integration_mode - integration_mode = config_dict.get('integration_mode', 'N/A') # Default to 'collection' - if integration_mode not in supported_integration_modes: - raise RuntimeWarning( - f"Unsupported integration_mode '{integration_mode}'. Supported modes are {supported_integration_modes}. Setting '{integration_mode}' to 'single_experiment'." - ) - - - # Validate datetime_steps format if it exists - if 'datetime_steps' in config_dict: - datetime_steps = config_dict['datetime_steps'] - expected_format = '%Y-%m-%d %H-%M-%S' - - # Check if datetime_steps is a list or a falsy value - if datetime_steps and not isinstance(datetime_steps, list): - raise TypeError(f"datetime_steps should be a list of strings or a falsy value (None, empty), but got {type(datetime_steps)}") - - for step_idx, step in enumerate(datetime_steps): - try: - # Attempt to parse the datetime to ensure correct format - config_dict['datetime_steps'][step_idx] = datetime.strptime(step, expected_format) - except ValueError: - raise ValueError(f"Invalid datetime format for '{step}'. Expected format: {expected_format}") - # Augment datatime_steps list as a dictionary. This to speed up single-experiment file generation - config_dict['datetime_steps_dict'] = _generate_datetime_dict(datetime_steps) - else: - # If datetime_steps is not present, set the integration mode to 'collection' - logging.info("datetime_steps missing, setting integration_mode to 'collection'.") - config_dict['integration_mode'] = 'collection' - - # Validate filename_format if defined - if 'filename_format' in config_dict: - if not isinstance(config_dict['filename_format'], str): - raise ValueError(f'"Specified filename_format needs to be of String type" ') - - # Split the string and check if each key exists in config_dict - keys = [key.strip() for key in config_dict['filename_format'].split(',')] - missing_keys = [key for key in keys if key not in config_dict] - - # If there are any missing keys, raise an assertion error - # assert not missing_keys, f'Missing key(s) in config_dict: {", ".join(missing_keys)}' - if not missing_keys: - config_dict['filename_format'] = ','.join(keys) - else: - config_dict['filename_format'] = None - print(f'"filename_format" should contain comma-separated keys that match existing keys in the YAML config file.') - print('Setting "filename_format" as None') - else: - config_dict['filename_format'] = None - - # Compute complementary metadata elements - - # Create output filename prefix - if not config_dict['filename_format']: # default behavior - config_dict['filename_prefix'] = '_'.join([config_dict[key] for key in ['experiment', 'contact']]) - else: - config_dict['filename_prefix'] = '_'.join([config_dict[key] for key in config_dict['filename_format'].split(sep=',')]) - - # Set default dates from datetime_steps if not provided - current_date = datetime.now().strftime('%Y-%m-%d') - dates = config_dict.get('datetime_steps',[]) - if not config_dict.get('dataset_startdate'): - config_dict['dataset_startdate'] = min(config_dict['datetime_steps']).strftime('%Y-%m-%d') if dates else current_date # Earliest datetime step - - if not config_dict.get('dataset_enddate'): - config_dict['dataset_enddate'] = max(config_dict['datetime_steps']).strftime('%Y-%m-%d') if dates else current_date # Latest datetime step - - config_dict['expected_datetime_format'] = '%Y-%m-%d %H-%M-%S' - - return config_dict - - -def copy_subtree_and_create_hdf5(src, dst, select_dir_keywords, select_file_keywords, allowed_file_extensions, root_metadata_dict): - - """Helper function to copy directory with constraints and create HDF5.""" - src = src.replace(os.sep,'/') - dst = dst.replace(os.sep,'/') - - logging.info("Creating constrained copy of the experimental campaign folder %s at: %s", src, dst) - - path_to_files_dict = utils.copy_directory_with_contraints(src, dst, select_dir_keywords, select_file_keywords, allowed_file_extensions) - logging.info("Finished creating a copy of the experimental campaign folder tree at: %s", dst) - - - logging.info("Creating HDF5 file at: %s", dst) - hdf5_path = hdf5_lib.create_hdf5_file_from_filesystem_path(dst, path_to_files_dict, select_dir_keywords, root_metadata_dict) - logging.info("Completed creation of HDF5 file %s at: %s", hdf5_path, dst) - - return hdf5_path - - -def run_pipeline(path_to_config_yamlFile, log_dir='logs/'): - - """Integrates data sources specified by the input configuration file into HDF5 files. - - Parameters: - yaml_config_file_path (str): Path to the YAML configuration file. - log_dir (str): Directory to save the log file. - - Returns: - list: List of Paths to the created HDF5 file(s). - """ - - config_dict = load_config_and_setup_logging(path_to_config_yamlFile, log_dir) - - path_to_input_dir = config_dict['input_file_directory'] - path_to_output_dir = config_dict['output_file_directory'] - select_dir_keywords = config_dict['instrument_datafolder'] - - # Define root folder metadata dictionary - root_metadata_dict = {key : config_dict[key] for key in ['project', 'experiment', 'contact', 'actris_level']} - - # Get dataset start and end dates - dataset_startdate = config_dict['dataset_startdate'] - dataset_enddate = config_dict['dataset_enddate'] - - # Determine mode and process accordingly - output_filename_path = [] - campaign_name_template = lambda filename_prefix, suffix: '_'.join([filename_prefix, suffix]) - date_str = f'{dataset_startdate}_{dataset_enddate}' - - # Create path to new raw datafolder and standardize with forward slashes - path_to_rawdata_folder = os.path.join( - path_to_output_dir, 'collection_' + campaign_name_template(config_dict['filename_prefix'], date_str), "").replace(os.sep, '/') - - # Process individual datetime steps if available, regardless of mode - if config_dict.get('datetime_steps_dict', {}): - # Single experiment mode - for datetime_step, file_keywords in config_dict['datetime_steps_dict'].items(): - date_str = datetime_step.strftime('%Y-%m-%d') - single_campaign_name = campaign_name_template(config_dict['filename_prefix'], date_str) - path_to_rawdata_subfolder = os.path.join(path_to_rawdata_folder, single_campaign_name, "") - - path_to_integrated_stepwise_hdf5_file = copy_subtree_and_create_hdf5( - path_to_input_dir, path_to_rawdata_subfolder, select_dir_keywords, - file_keywords, allowed_file_extensions, root_metadata_dict) - - output_filename_path.append(path_to_integrated_stepwise_hdf5_file) - - # Collection mode processing if specified - if 'collection' in config_dict.get('integration_mode', 'single_experiment'): - path_to_filenames_dict = {path_to_rawdata_folder: [os.path.basename(path) for path in output_filename_path]} if output_filename_path else {} - hdf5_path = hdf5_lib.create_hdf5_file_from_filesystem_path(path_to_rawdata_folder, path_to_filenames_dict, [], root_metadata_dict) - output_filename_path.append(hdf5_path) - else: - path_to_integrated_stepwise_hdf5_file = copy_subtree_and_create_hdf5( - path_to_input_dir, path_to_rawdata_folder, select_dir_keywords, [], - allowed_file_extensions, root_metadata_dict) - output_filename_path.append(path_to_integrated_stepwise_hdf5_file) - - return output_filename_path - - -if __name__ == "__main__": - - if len(sys.argv) < 2: - print("Usage: python data_integration.py ") - sys.exit(1) - - # Extract the function name from the command line arguments - function_name = sys.argv[1] - - # Handle function execution based on the provided function name - if function_name == 'run': - - if len(sys.argv) != 3: - print("Usage: python data_integration.py run ") - sys.exit(1) - # Extract path to configuration file, specifying the data integration task - path_to_config_yamlFile = sys.argv[2] - run_pipeline(path_to_config_yamlFile) - - +import sys +import os +import re + +try: + thisFilePath = os.path.abspath(__file__) +except NameError: + print("Error: __file__ is not available. Ensure the script is being run from a file.") + print("[Notice] Path to DIMA package may not be resolved properly.") + thisFilePath = os.getcwd() # Use current directory or specify a default + +dimaPath = os.path.normpath(os.path.join(thisFilePath, "..",'..')) # Move up to project root + +if dimaPath not in sys.path: # Avoid duplicate entries + sys.path.append(dimaPath) + + +import yaml +import logging +from datetime import datetime +# Importing chain class from itertools +from itertools import chain + +# Import DIMA modules +import src.hdf5_writer as hdf5_lib +import utils.g5505_utils as utils +from instruments.readers import filereader_registry + +allowed_file_extensions = filereader_registry.file_extensions + +def _generate_datetime_dict(datetime_steps): + """ Generate the datetime augment dictionary from datetime steps. """ + datetime_augment_dict = {} + for datetime_step in datetime_steps: + #tmp = datetime.strptime(datetime_step, '%Y-%m-%d %H-%M-%S') + datetime_augment_dict[datetime_step] = [ + datetime_step.strftime('%Y-%m-%d'), datetime_step.strftime('%Y_%m_%d'), datetime_step.strftime('%Y.%m.%d'), datetime_step.strftime('%Y%m%d') + ] + return datetime_augment_dict + +def load_config_and_setup_logging(yaml_config_file_path, log_dir): + """Load YAML configuration file, set up logging, and validate required keys and datetime_steps.""" + + # Define required keys + required_keys = [ + 'experiment', 'contact', 'input_file_directory', 'output_file_directory', + 'instrument_datafolder', 'project', 'actris_level' + ] + + # Supported integration modes + supported_integration_modes = ['collection', 'single_experiment'] + + + # Set up logging + date = utils.created_at("%Y_%m").replace(":", "-") + utils.setup_logging(log_dir, f"integrate_data_sources_{date}.log") + + # Load YAML configuration file + with open(yaml_config_file_path, 'r') as stream: + try: + config_dict = yaml.load(stream, Loader=yaml.FullLoader) + except yaml.YAMLError as exc: + logging.error("Error loading YAML file: %s", exc) + raise ValueError(f"Failed to load YAML file: {exc}") + + # Check if required keys are present + missing_keys = [key for key in required_keys if key not in config_dict] + if missing_keys: + raise KeyError(f"Missing required keys in YAML configuration: {missing_keys}") + + # Check the instrument_datafolder required type and ensure the list is of at least length one. + if isinstance(config_dict['instrument_datafolder'], list) and not len(config_dict['instrument_datafolder'])>=1: + raise ValueError('Invalid value for key "instrument_datafolder". Expected a list of strings with at least one item.' + 'Each item represents a subfolder name in the input file directory, where the name' + 'must match the format "[/]".' + 'The first subfolder name is required, and the second is optional. ' + 'Examples of valid values: "level1", "level1/level2".') + + # Define the pattern for valid subfolder names: `subfolder` or `subfolder/subfolder` + #valid_pattern = re.compile(r'^[^/]+(/[^/]+)?$') + + # Validate each subfolder name + #for folder in config_dict['instrument_folder']: + # if not isinstance(folder, str) or not valid_pattern.match(folder): + # raise ValueError( + # 'Invalid value for key "instrument_folder" in YAML file.' + # 'Each item must be a string matching the format ' + # '"[/]". The first subfolder name is required, and the second is optional. ' + # 'Examples of valid values: "level1", "level1/level2". ' + # f'Invalid item: {folder}' + # ) + + # Validate integration_mode + integration_mode = config_dict.get('integration_mode', 'N/A') # Default to 'collection' + if integration_mode not in supported_integration_modes: + raise RuntimeWarning( + f"Unsupported integration_mode '{integration_mode}'. Supported modes are {supported_integration_modes}. Setting '{integration_mode}' to 'single_experiment'." + ) + + + # Validate datetime_steps format if it exists + if 'datetime_steps' in config_dict: + datetime_steps = config_dict['datetime_steps'] + expected_format = '%Y-%m-%d %H-%M-%S' + + # Check if datetime_steps is a list or a falsy value + if datetime_steps and not isinstance(datetime_steps, list): + raise TypeError(f"datetime_steps should be a list of strings or a falsy value (None, empty), but got {type(datetime_steps)}") + + for step_idx, step in enumerate(datetime_steps): + try: + # Attempt to parse the datetime to ensure correct format + config_dict['datetime_steps'][step_idx] = datetime.strptime(step, expected_format) + except ValueError: + raise ValueError(f"Invalid datetime format for '{step}'. Expected format: {expected_format}") + # Augment datatime_steps list as a dictionary. This to speed up single-experiment file generation + config_dict['datetime_steps_dict'] = _generate_datetime_dict(datetime_steps) + else: + # If datetime_steps is not present, set the integration mode to 'collection' + logging.info("datetime_steps missing, setting integration_mode to 'collection'.") + config_dict['integration_mode'] = 'collection' + + # Validate filename_format if defined + if 'filename_format' in config_dict: + if not isinstance(config_dict['filename_format'], str): + raise ValueError(f'"Specified filename_format needs to be of String type" ') + + # Split the string and check if each key exists in config_dict + keys = [key.strip() for key in config_dict['filename_format'].split(',')] + missing_keys = [key for key in keys if key not in config_dict] + + # If there are any missing keys, raise an assertion error + # assert not missing_keys, f'Missing key(s) in config_dict: {", ".join(missing_keys)}' + if not missing_keys: + config_dict['filename_format'] = ','.join(keys) + else: + config_dict['filename_format'] = None + print(f'"filename_format" should contain comma-separated keys that match existing keys in the YAML config file.') + print('Setting "filename_format" as None') + else: + config_dict['filename_format'] = None + + # Compute complementary metadata elements + + # Create output filename prefix + if not config_dict['filename_format']: # default behavior + config_dict['filename_prefix'] = '_'.join([config_dict[key] for key in ['experiment', 'contact']]) + else: + config_dict['filename_prefix'] = '_'.join([config_dict[key] for key in config_dict['filename_format'].split(sep=',')]) + + # Set default dates from datetime_steps if not provided + current_date = datetime.now().strftime('%Y-%m-%d') + dates = config_dict.get('datetime_steps',[]) + if not config_dict.get('dataset_startdate'): + config_dict['dataset_startdate'] = min(config_dict['datetime_steps']).strftime('%Y-%m-%d') if dates else current_date # Earliest datetime step + + if not config_dict.get('dataset_enddate'): + config_dict['dataset_enddate'] = max(config_dict['datetime_steps']).strftime('%Y-%m-%d') if dates else current_date # Latest datetime step + + config_dict['expected_datetime_format'] = '%Y-%m-%d %H-%M-%S' + + return config_dict + + +def copy_subtree_and_create_hdf5(src, dst, select_dir_keywords, select_file_keywords, allowed_file_extensions, root_metadata_dict): + + """Helper function to copy directory with constraints and create HDF5.""" + src = src.replace(os.sep,'/') + dst = dst.replace(os.sep,'/') + + logging.info("Creating constrained copy of the experimental campaign folder %s at: %s", src, dst) + + path_to_files_dict = utils.copy_directory_with_contraints(src, dst, select_dir_keywords, select_file_keywords, allowed_file_extensions) + logging.info("Finished creating a copy of the experimental campaign folder tree at: %s", dst) + + + logging.info("Creating HDF5 file at: %s", dst) + hdf5_path = hdf5_lib.create_hdf5_file_from_filesystem_path(dst, path_to_files_dict, select_dir_keywords, root_metadata_dict) + logging.info("Completed creation of HDF5 file %s at: %s", hdf5_path, dst) + + return hdf5_path + + +def run_pipeline(path_to_config_yamlFile, log_dir='logs/'): + + """Integrates data sources specified by the input configuration file into HDF5 files. + + Parameters: + yaml_config_file_path (str): Path to the YAML configuration file. + log_dir (str): Directory to save the log file. + + Returns: + list: List of Paths to the created HDF5 file(s). + """ + + config_dict = load_config_and_setup_logging(path_to_config_yamlFile, log_dir) + + path_to_input_dir = config_dict['input_file_directory'] + path_to_output_dir = config_dict['output_file_directory'] + select_dir_keywords = config_dict['instrument_datafolder'] + + # Define root folder metadata dictionary + root_metadata_dict = {key : config_dict[key] for key in ['project', 'experiment', 'contact', 'actris_level']} + + # Get dataset start and end dates + dataset_startdate = config_dict['dataset_startdate'] + dataset_enddate = config_dict['dataset_enddate'] + + # Determine mode and process accordingly + output_filename_path = [] + campaign_name_template = lambda filename_prefix, suffix: '_'.join([filename_prefix, suffix]) + date_str = f'{dataset_startdate}_{dataset_enddate}' + + # Create path to new raw datafolder and standardize with forward slashes + path_to_rawdata_folder = os.path.join( + path_to_output_dir, 'collection_' + campaign_name_template(config_dict['filename_prefix'], date_str), "").replace(os.sep, '/') + + # Process individual datetime steps if available, regardless of mode + if config_dict.get('datetime_steps_dict', {}): + # Single experiment mode + for datetime_step, file_keywords in config_dict['datetime_steps_dict'].items(): + date_str = datetime_step.strftime('%Y-%m-%d') + single_campaign_name = campaign_name_template(config_dict['filename_prefix'], date_str) + path_to_rawdata_subfolder = os.path.join(path_to_rawdata_folder, single_campaign_name, "") + + path_to_integrated_stepwise_hdf5_file = copy_subtree_and_create_hdf5( + path_to_input_dir, path_to_rawdata_subfolder, select_dir_keywords, + file_keywords, allowed_file_extensions, root_metadata_dict) + + output_filename_path.append(path_to_integrated_stepwise_hdf5_file) + + # Collection mode processing if specified + if 'collection' in config_dict.get('integration_mode', 'single_experiment'): + path_to_filenames_dict = {path_to_rawdata_folder: [os.path.basename(path) for path in output_filename_path]} if output_filename_path else {} + hdf5_path = hdf5_lib.create_hdf5_file_from_filesystem_path(path_to_rawdata_folder, path_to_filenames_dict, [], root_metadata_dict) + output_filename_path.append(hdf5_path) + else: + path_to_integrated_stepwise_hdf5_file = copy_subtree_and_create_hdf5( + path_to_input_dir, path_to_rawdata_folder, select_dir_keywords, [], + allowed_file_extensions, root_metadata_dict) + output_filename_path.append(path_to_integrated_stepwise_hdf5_file) + + return output_filename_path + + +if __name__ == "__main__": + + if len(sys.argv) < 2: + print("Usage: python data_integration.py ") + sys.exit(1) + + # Extract the function name from the command line arguments + function_name = sys.argv[1] + + # Handle function execution based on the provided function name + if function_name == 'run': + + if len(sys.argv) != 3: + print("Usage: python data_integration.py run ") + sys.exit(1) + # Extract path to configuration file, specifying the data integration task + path_to_config_yamlFile = sys.argv[2] + run_pipeline(path_to_config_yamlFile) + + diff --git a/pipelines/metadata_revision.py b/pipelines/metadata_revision.py index 363243e..0089dc6 100644 --- a/pipelines/metadata_revision.py +++ b/pipelines/metadata_revision.py @@ -1,179 +1,179 @@ -import sys -import os - -try: - thisFilePath = os.path.abspath(__file__) -except NameError: - print("Error: __file__ is not available. Ensure the script is being run from a file.") - print("[Notice] Path to DIMA package may not be resolved properly.") - thisFilePath = os.getcwd() # Use current directory or specify a default - -dimaPath = os.path.normpath(os.path.join(thisFilePath, "..",'..')) # Move up to project root - -if dimaPath not in sys.path: # Avoid duplicate entries - sys.path.append(dimaPath) - -import h5py -import yaml -import src.hdf5_ops as hdf5_ops - - -def load_yaml(review_yaml_file): - with open(review_yaml_file, 'r') as stream: - try: - return yaml.load(stream, Loader=yaml.FullLoader) - except yaml.YAMLError as exc: - print(exc) - return None - -def validate_yaml_dict(input_hdf5_file, yaml_dict): - errors = [] - notes = [] - - with h5py.File(input_hdf5_file, 'r') as hdf5_file: - # 1. Check for valid object names - for key in yaml_dict: - if key not in hdf5_file: - error_msg = f"Error: {key} is not a valid object's name in the HDF5 file." - print(error_msg) - errors.append(error_msg) - - # 2. Confirm metadata dict for each object is a dictionary - for key, meta_dict in yaml_dict.items(): - if not isinstance(meta_dict, dict): - error_msg = f"Error: Metadata for {key} should be a dictionary." - print(error_msg) - errors.append(error_msg) - else: - if 'attributes' not in meta_dict: - warning_msg = f"Warning: No 'attributes' in metadata dict for {key}." - print(warning_msg) - notes.append(warning_msg) - - # 3. Verify update, append, and delete operations are well specified - for key, meta_dict in yaml_dict.items(): - attributes = meta_dict.get("attributes", {}) - - for attr_name, attr_value in attributes.items(): - # Ensure the object exists before accessing attributes - if key in hdf5_file: - hdf5_obj_attrs = hdf5_file[key].attrs # Access object-specific attributes - - if attr_name in hdf5_obj_attrs: - # Attribute exists: it can be updated or deleted - if isinstance(attr_value, dict) and "delete" in attr_value: - note_msg = f"Note: '{attr_name}' in {key} may be deleted if 'delete' is set as true." - print(note_msg) - notes.append(note_msg) - else: - note_msg = f"Note: '{attr_name}' in {key} will be updated." - print(note_msg) - notes.append(note_msg) - else: - # Attribute does not exist: it can be appended or flagged as an invalid delete - if isinstance(attr_value, dict) and "delete" in attr_value: - error_msg = f"Error: Cannot delete non-existent attribute '{attr_name}' in {key}." - print(error_msg) - errors.append(error_msg) - else: - note_msg = f"Note: '{attr_name}' in {key} will be appended." - print(note_msg) - notes.append(note_msg) - else: - error_msg = f"Error: '{key}' is not a valid object in the HDF5 file." - print(error_msg) - errors.append(error_msg) - - return len(errors) == 0, errors, notes - - -def update_hdf5_file_with_review(input_hdf5_file, review_yaml_file): - - """ - Updates, appends, or deletes metadata attributes in an HDF5 file based on a provided YAML dictionary. - - Parameters: - ----------- - input_hdf5_file : str - Path to the HDF5 file. - - yaml_dict : dict - Dictionary specifying objects and their attributes with operations. Example format: - { - "object_name": { "attributes" : "attr_name": { "value": attr_value, - "delete": true | false - } - } - } - """ - yaml_dict = load_yaml(review_yaml_file) - - success, errors, notes = validate_yaml_dict(input_hdf5_file,yaml_dict) - if not success: - raise ValueError(f"Review yaml file {review_yaml_file} is invalid. Validation errors: {errors}") - - # Initialize HDF5 operations manager - DataOpsAPI = hdf5_ops.HDF5DataOpsManager(input_hdf5_file) - DataOpsAPI.load_file_obj() - - # Iterate over each object in the YAML dictionary - for obj_name, attr_dict in yaml_dict.items(): - # Prepare dictionaries for append, update, and delete actions - append_dict = {} - update_dict = {} - delete_dict = {} - - if not obj_name in DataOpsAPI.file_obj: - continue # Skip if the object does not exist - - # Iterate over each attribute in the current object - for attr_name, attr_props in attr_dict['attributes'].items(): - if not isinstance(attr_props, dict): - #attr_props = {'value': attr_props} - # Check if the attribute exists (for updating) - if attr_name in DataOpsAPI.file_obj[obj_name].attrs: - update_dict[attr_name] = attr_props - # Otherwise, it's a new attribute to append - else: - append_dict[attr_name] = attr_props - else: - # Check if the attribute is marked for deletion - if attr_props.get('delete', False): - delete_dict[attr_name] = attr_props - - # Perform a single pass for all three operations - if append_dict: - DataOpsAPI.append_metadata(obj_name, append_dict) - if update_dict: - DataOpsAPI.update_metadata(obj_name, update_dict) - if delete_dict: - DataOpsAPI.delete_metadata(obj_name, delete_dict) - - # Close hdf5 file - DataOpsAPI.unload_file_obj() - # Regenerate yaml snapshot of updated HDF5 file - output_yml_filename_path = hdf5_ops.serialize_metadata(input_hdf5_file) - print(f'{output_yml_filename_path} was successfully regenerated from the updated version of{input_hdf5_file}') - -def count(hdf5_obj,yml_dict): - print(hdf5_obj.name) - if isinstance(hdf5_obj,h5py.Group) and len(hdf5_obj.name.split('/')) <= 4: - obj_review = yml_dict[hdf5_obj.name] - additions = [not (item in hdf5_obj.attrs.keys()) for item in obj_review['attributes'].keys()] - count_additions = sum(additions) - deletions = [not (item in obj_review['attributes'].keys()) for item in hdf5_obj.attrs.keys()] - count_delections = sum(deletions) - print('additions',count_additions, 'deletions', count_delections) - -if __name__ == "__main__": - - if len(sys.argv) < 4: - print("Usage: python metadata_revision.py update ") - sys.exit(1) - - - if sys.argv[1] == 'update': - input_hdf5_file = sys.argv[2] - review_yaml_file = sys.argv[3] - update_hdf5_file_with_review(input_hdf5_file, review_yaml_file) - #run(sys.argv[2]) +import sys +import os + +try: + thisFilePath = os.path.abspath(__file__) +except NameError: + print("Error: __file__ is not available. Ensure the script is being run from a file.") + print("[Notice] Path to DIMA package may not be resolved properly.") + thisFilePath = os.getcwd() # Use current directory or specify a default + +dimaPath = os.path.normpath(os.path.join(thisFilePath, "..",'..')) # Move up to project root + +if dimaPath not in sys.path: # Avoid duplicate entries + sys.path.append(dimaPath) + +import h5py +import yaml +import src.hdf5_ops as hdf5_ops + + +def load_yaml(review_yaml_file): + with open(review_yaml_file, 'r') as stream: + try: + return yaml.load(stream, Loader=yaml.FullLoader) + except yaml.YAMLError as exc: + print(exc) + return None + +def validate_yaml_dict(input_hdf5_file, yaml_dict): + errors = [] + notes = [] + + with h5py.File(input_hdf5_file, 'r') as hdf5_file: + # 1. Check for valid object names + for key in yaml_dict: + if key not in hdf5_file: + error_msg = f"Error: {key} is not a valid object's name in the HDF5 file." + print(error_msg) + errors.append(error_msg) + + # 2. Confirm metadata dict for each object is a dictionary + for key, meta_dict in yaml_dict.items(): + if not isinstance(meta_dict, dict): + error_msg = f"Error: Metadata for {key} should be a dictionary." + print(error_msg) + errors.append(error_msg) + else: + if 'attributes' not in meta_dict: + warning_msg = f"Warning: No 'attributes' in metadata dict for {key}." + print(warning_msg) + notes.append(warning_msg) + + # 3. Verify update, append, and delete operations are well specified + for key, meta_dict in yaml_dict.items(): + attributes = meta_dict.get("attributes", {}) + + for attr_name, attr_value in attributes.items(): + # Ensure the object exists before accessing attributes + if key in hdf5_file: + hdf5_obj_attrs = hdf5_file[key].attrs # Access object-specific attributes + + if attr_name in hdf5_obj_attrs: + # Attribute exists: it can be updated or deleted + if isinstance(attr_value, dict) and "delete" in attr_value: + note_msg = f"Note: '{attr_name}' in {key} may be deleted if 'delete' is set as true." + print(note_msg) + notes.append(note_msg) + else: + note_msg = f"Note: '{attr_name}' in {key} will be updated." + print(note_msg) + notes.append(note_msg) + else: + # Attribute does not exist: it can be appended or flagged as an invalid delete + if isinstance(attr_value, dict) and "delete" in attr_value: + error_msg = f"Error: Cannot delete non-existent attribute '{attr_name}' in {key}." + print(error_msg) + errors.append(error_msg) + else: + note_msg = f"Note: '{attr_name}' in {key} will be appended." + print(note_msg) + notes.append(note_msg) + else: + error_msg = f"Error: '{key}' is not a valid object in the HDF5 file." + print(error_msg) + errors.append(error_msg) + + return len(errors) == 0, errors, notes + + +def update_hdf5_file_with_review(input_hdf5_file, review_yaml_file): + + """ + Updates, appends, or deletes metadata attributes in an HDF5 file based on a provided YAML dictionary. + + Parameters: + ----------- + input_hdf5_file : str + Path to the HDF5 file. + + yaml_dict : dict + Dictionary specifying objects and their attributes with operations. Example format: + { + "object_name": { "attributes" : "attr_name": { "value": attr_value, + "delete": true | false + } + } + } + """ + yaml_dict = load_yaml(review_yaml_file) + + success, errors, notes = validate_yaml_dict(input_hdf5_file,yaml_dict) + if not success: + raise ValueError(f"Review yaml file {review_yaml_file} is invalid. Validation errors: {errors}") + + # Initialize HDF5 operations manager + DataOpsAPI = hdf5_ops.HDF5DataOpsManager(input_hdf5_file) + DataOpsAPI.load_file_obj() + + # Iterate over each object in the YAML dictionary + for obj_name, attr_dict in yaml_dict.items(): + # Prepare dictionaries for append, update, and delete actions + append_dict = {} + update_dict = {} + delete_dict = {} + + if not obj_name in DataOpsAPI.file_obj: + continue # Skip if the object does not exist + + # Iterate over each attribute in the current object + for attr_name, attr_props in attr_dict['attributes'].items(): + if not isinstance(attr_props, dict): + #attr_props = {'value': attr_props} + # Check if the attribute exists (for updating) + if attr_name in DataOpsAPI.file_obj[obj_name].attrs: + update_dict[attr_name] = attr_props + # Otherwise, it's a new attribute to append + else: + append_dict[attr_name] = attr_props + else: + # Check if the attribute is marked for deletion + if attr_props.get('delete', False): + delete_dict[attr_name] = attr_props + + # Perform a single pass for all three operations + if append_dict: + DataOpsAPI.append_metadata(obj_name, append_dict) + if update_dict: + DataOpsAPI.update_metadata(obj_name, update_dict) + if delete_dict: + DataOpsAPI.delete_metadata(obj_name, delete_dict) + + # Close hdf5 file + DataOpsAPI.unload_file_obj() + # Regenerate yaml snapshot of updated HDF5 file + output_yml_filename_path = hdf5_ops.serialize_metadata(input_hdf5_file) + print(f'{output_yml_filename_path} was successfully regenerated from the updated version of{input_hdf5_file}') + +def count(hdf5_obj,yml_dict): + print(hdf5_obj.name) + if isinstance(hdf5_obj,h5py.Group) and len(hdf5_obj.name.split('/')) <= 4: + obj_review = yml_dict[hdf5_obj.name] + additions = [not (item in hdf5_obj.attrs.keys()) for item in obj_review['attributes'].keys()] + count_additions = sum(additions) + deletions = [not (item in obj_review['attributes'].keys()) for item in hdf5_obj.attrs.keys()] + count_delections = sum(deletions) + print('additions',count_additions, 'deletions', count_delections) + +if __name__ == "__main__": + + if len(sys.argv) < 4: + print("Usage: python metadata_revision.py update ") + sys.exit(1) + + + if sys.argv[1] == 'update': + input_hdf5_file = sys.argv[2] + review_yaml_file = sys.argv[3] + update_hdf5_file_with_review(input_hdf5_file, review_yaml_file) + #run(sys.argv[2]) diff --git a/setup_env.sh b/setup_env.sh index 491175d..9e7e8a6 100644 --- a/setup_env.sh +++ b/setup_env.sh @@ -1,47 +1,47 @@ -#!/bin/bash - -# Define the name of the environment -ENV_NAME="multiphase_chemistry_env" - -# Check if mamba is available and use it instead of conda for faster installation -if command -v mamba &> /dev/null; then - CONDA_COMMAND="mamba" -else - CONDA_COMMAND="conda" -fi - -# Create the conda environment with all dependencies, resolving from conda-forge and defaults -$CONDA_COMMAND create -y -n "$ENV_NAME" -c conda-forge -c defaults python=3.11 \ - jupyter numpy h5py pandas matplotlib plotly=5.24 scipy pip - -# Check if the environment was successfully created -if [ $? -ne 0 ]; then - echo "Failed to create the environment '$ENV_NAME'. Please check the logs above for details." - exit 1 -fi - -# Activate the new environment -if source activate "$ENV_NAME" 2>/dev/null || conda activate "$ENV_NAME" 2>/dev/null; then - echo "Environment '$ENV_NAME' activated successfully." -else - echo "Failed to activate the environment '$ENV_NAME'. Please check your conda setup." - exit 1 -fi - -# Install additional pip packages only if the environment is activated -echo "Installing additional pip packages..." -pip install pybis==1.35 igor2 ipykernel sphinx - -# Check if pip installations were successful -if [ $? -ne 0 ]; then - echo "Failed to install pip packages. Please check the logs above for details." - exit 1 -fi - -# Optional: Export the environment to a YAML file (commented out) -# $CONDA_COMMAND env export -n "$ENV_NAME" > "$ENV_NAME-environment.yaml" - -# Print success message -echo "Environment '$ENV_NAME' created and configured successfully." -# echo "Environment configuration saved to '$ENV_NAME-environment.yaml'." - +#!/bin/bash + +# Define the name of the environment +ENV_NAME="multiphase_chemistry_env" + +# Check if mamba is available and use it instead of conda for faster installation +if command -v mamba &> /dev/null; then + CONDA_COMMAND="mamba" +else + CONDA_COMMAND="conda" +fi + +# Create the conda environment with all dependencies, resolving from conda-forge and defaults +$CONDA_COMMAND create -y -n "$ENV_NAME" -c conda-forge -c defaults python=3.11 \ + jupyter numpy h5py pandas matplotlib plotly=5.24 scipy pip + +# Check if the environment was successfully created +if [ $? -ne 0 ]; then + echo "Failed to create the environment '$ENV_NAME'. Please check the logs above for details." + exit 1 +fi + +# Activate the new environment +if source activate "$ENV_NAME" 2>/dev/null || conda activate "$ENV_NAME" 2>/dev/null; then + echo "Environment '$ENV_NAME' activated successfully." +else + echo "Failed to activate the environment '$ENV_NAME'. Please check your conda setup." + exit 1 +fi + +# Install additional pip packages only if the environment is activated +echo "Installing additional pip packages..." +pip install pybis==1.35 igor2 ipykernel sphinx + +# Check if pip installations were successful +if [ $? -ne 0 ]; then + echo "Failed to install pip packages. Please check the logs above for details." + exit 1 +fi + +# Optional: Export the environment to a YAML file (commented out) +# $CONDA_COMMAND env export -n "$ENV_NAME" > "$ENV_NAME-environment.yaml" + +# Print success message +echo "Environment '$ENV_NAME' created and configured successfully." +# echo "Environment configuration saved to '$ENV_NAME-environment.yaml'." + diff --git a/src/git_ops.py b/src/git_ops.py index c506513..b7395dc 100644 --- a/src/git_ops.py +++ b/src/git_ops.py @@ -1,358 +1,358 @@ -import subprocess -import os -import utils.g5505_utils as utils -from pipelines.metadata_revision import update_hdf5_file_with_review - -def perform_git_operations(hdf5_upload): - status_command = ['git', 'status'] - status = subprocess.run(status_command, capture_output=True, check=True) - - if hdf5_upload: - upload_ext = ['.h5', '.yaml'] - else: - upload_ext = ['.yaml'] - - files_to_add_list = extract_files_to_add(status.stdout, upload_ext) - if files_to_add_list: - add_files_to_git(files_to_add_list) - commit_changes('Updated hdf5 file with yaml review file.') - else: - print("There were no found h5 and yaml files, needing to be saved. This action will not have effect on the review process' commit history.") - -def extract_files_to_add(git_status_output, upload_ext): - files_to_add_list = [] - for line in git_status_output.splitlines(): - tmp = line.decode("utf-8") - if 'modified' in tmp: - if any(ext in tmp for ext in upload_ext): - files_to_add_list.append(tmp.split()[1]) - return files_to_add_list - -def add_files_to_git(files_to_add_list): - add_command = ['git', 'add'] + files_to_add_list - subprocess.run(add_command, capture_output=True, check=True) - -def commit_changes(message): - commit_command = ['git', 'commit', '-m', message] - commit_output = subprocess.run(commit_command, capture_output=True, check=True) - print(commit_output.stdout) - -def get_status(): - return subprocess.run(['git','status'],capture_output=True,text=True,check=True) - -def show_current_branch(): - current_branch_command = ['git','branch','--show-current'] - subprocess.run(current_branch_command,capture_output=True,text=True,check=True) - - - -YAML_EXT = ".yaml" -TXT_EXT = ".txt" - - - -def get_review_status(filename_path): - - filename_path_tail, filename_path_head = os.path.split(filename_path) - filename, ext = os.path.splitext(filename_path_head) - # TODO: - with open(os.path.join("review/",filename+"-review_status"+TXT_EXT),'r') as f: - workflow_steps = [] - for line in f: - workflow_steps.append(line) - return workflow_steps[-1] - -def first_initialize_metadata_review(hdf5_file_path, reviewer_attrs, restart = False): - - """ - First: Initialize review branch with review folder with a copy of yaml representation of - hdf5 file under review and by creating a txt file with the state of the review process, e.g., under review. - - """ - - initials = reviewer_attrs['initials'] - #branch_name = '-'.join([reviewer_attrs['type'],'review_',initials]) - branch_name = '_'.join(['review',initials]) - - hdf5_file_path_tail, filename_path_head = os.path.split(hdf5_file_path) - filename, ext = os.path.splitext(filename_path_head) - - # Check file_path points to h5 file - if not 'h5' in ext: - raise ValueError("filename_path needs to point to an h5 file.") - - # Verify if yaml snapshot of input h5 file exists - if not os.path.exists(os.path.join(hdf5_file_path_tail,filename+YAML_EXT)): - raise ValueError("metadata review cannot be initialized. The associated .yaml file under review was not found. Run serialize_metadata(filename_path) ") - - # Initialize metadata review workflow - # print("Create branch metadata-review-by-"+initials+"\n") - - #checkout_review_branch(branch_name) - - # Check you are working at the right branch - - curr_branch = show_current_branch() - if not branch_name in curr_branch.stdout: - raise ValueError("Branch "+branch_name+" was not found. \nPlease open a Git Bash Terminal, and follow the below instructions: \n1. Change directory to your project's directory. \n2. Excecute the command: git checkout "+branch_name) - - # Check if review file already exists and then check if it is still untracked - review_yaml_file_path = os.path.join("review/",filename+YAML_EXT) - review_yaml_file_path_tail, ext = os.path.splitext(review_yaml_file_path) - review_status_yaml_file_path = os.path.join(review_yaml_file_path_tail+"-review_status"+".txt") - - if not os.path.exists(review_yaml_file_path) or restart: - review_yaml_file_path = utils.make_file_copy(os.path.join(hdf5_file_path_tail,filename+YAML_EXT), 'review') - if restart: - print('metadata review has been reinitialized. The review files will reflect the current state of the hdf5 files metadata') - - - - #if not os.path.exists(os.path.join(review_yaml_file_path_tail+"-review_status"+".txt")): - - with open(review_status_yaml_file_path,'w') as f: - f.write('under review') - - # Stage untracked review files and commit them to local repository - status = get_status() - untracked_files = [] - for line in status.stdout.splitlines(): - #tmp = line.decode("utf-8") - #modified_files.append(tmp.split()[1]) - if 'review/' in line: - if not 'modified' in line: # untracked filesand - untracked_files.append(line.strip()) - else: - untracked_files.append(line.strip().split()[1]) - - if 'output_files/'+filename+YAML_EXT in line and not 'modified' in line: - untracked_files.append(line.strip()) - - if untracked_files: - result = subprocess.run(add_files_to_git(untracked_files),capture_output=True,check=True) - message = 'Initialized metadata review.' - commit_output = subprocess.run(commit_changes(message),capture_output=True,check=True) - - for line in commit_output.stdout.splitlines(): - print(line.decode('utf-8')) - #else: - # print('This action will not have any effect because metadata review process has been already initialized.') - - - - - #status_dict = repo_obj.status() - #for filepath, file_status in status_dict.items(): - # Identify keys associated to review files and stage them - # if 'review/'+filename in filepath: - # Stage changes - # repo_obj.index.add(filepath) - - #author = config_file.author #default_signature - #committer = config_file.committer - #message = "Initialized metadata review process." - #tree = repo_obj.index.write_tree() - #oid = repo_obj.create_commit('HEAD', author, committer, message, tree, [repo_obj.head.peel().oid]) - - #print("Add and commit"+"\n") - - return review_yaml_file_path, review_status_yaml_file_path - - - -def second_save_metadata_review(review_yaml_file_path, reviewer_attrs): - """ - Second: Once you're done reviewing the yaml representation of hdf5 file in review folder. - Change the review status to complete and save (add and commit) modified .yalm and .txt files in the project by - running this function. - - """ - # 1 verify review initializatin was performed first - # 2. change review status in txt to complete - # 3. git add review/ and git commit -m "Submitted metadata review" - - initials = reviewer_attrs['initials'] - #branch_name = '-'.join([reviewer_attrs['type'],'review','by',initials]) - branch_name = '_'.join(['review',initials]) - # TODO: replace with subprocess + git - #checkout_review_branch(repo_obj, branch_name) - - # Check you are working at the right branch - curr_branch = show_current_branch() - if not branch_name in curr_branch.stdout: - raise ValueError('Please checkout ' + branch_name + ' via Git Bash before submitting metadata review files. ') - - # Collect modified review files - status = get_status() - modified_files = [] - os.path.basename(review_yaml_file_path) - for line in status.stdout.splitlines(): - # conver line from bytes to str - tmp = line.decode("utf-8") - if 'modified' in tmp and 'review/' in tmp and os.path.basename(review_yaml_file_path) in tmp: - modified_files.append(tmp.split()[1]) - - # Stage modified files and commit them to local repository - review_yaml_file_path_tail, review_yaml_file_path_head = os.path.split(review_yaml_file_path) - filename, ext = os.path.splitext(review_yaml_file_path_head) - if modified_files: - review_status_file_path = os.path.join("review/",filename+"-review_status"+TXT_EXT) - with open(review_status_file_path,'a') as f: - f.write('\nsubmitted') - - modified_files.append(review_status_file_path) - - result = subprocess.run(add_files_to_git(modified_files),capture_output=True,check=True) - message = 'Submitted metadata review.' - commit_output = subprocess.run(commit_changes(message),capture_output=True,check=True) - - for line in commit_output.stdout.splitlines(): - print(line.decode('utf-8')) - else: - print('Nothing to commit.') - -# -def third_update_hdf5_file_with_review(input_hdf5_file, yaml_review_file, reviewer_attrs={}, hdf5_upload=False): - if 'submitted' not in get_review_status(input_hdf5_file): - raise ValueError('Review yaml file must be submitted before trying to perform an update. Run first second_submit_metadata_review().') - - update_hdf5_file_with_review(input_hdf5_file, yaml_review_file) - perform_git_operations(hdf5_upload) - -def last_submit_metadata_review(reviewer_attrs): - - """Fourth: """ - - initials =reviewer_attrs['initials'] - - repository = 'origin' - branch_name = '_'.join(['review',initials]) - - push_command = lambda repository,refspec: ['git','push',repository,refspec] - - list_branches_command = ['git','branch','--list'] - - branches = subprocess.run(list_branches_command,capture_output=True,text=True,check=True) - if not branch_name in branches.stdout: - print('There is no branch named '+branch_name+'.\n') - print('Make sure to run data owner review workflow from the beginning without missing any steps.') - return - - curr_branch = show_current_branch() - if not branch_name in curr_branch.stdout: - print('Complete metadata review could not be completed.\n') - print('Make sure a data-owner workflow has already been started on branch '+branch_name+'\n') - print('The step "Complete metadata review" will have no effect.') - return - - - - # push - result = subprocess.run(push_command(repository,branch_name),capture_output=True,text=True,check=True) - print(result.stdout) - - # 1. git add output_files/ - # 2. delete review/ - #shutil.rmtree(os.path.join(os.path.abspath(os.curdir),"review")) - # 3. git rm review/ - # 4. git commit -m "Completed review process. Current state of hdf5 file and yml should be up to date." - return result.returncode - - -#import config_file -#import hdf5_ops - -class MetadataHarvester: - def __init__(self, parent_files=None): - if parent_files is None: - parent_files = [] - self.parent_files = parent_files - self.metadata = { - "project": {}, - "sample": {}, - "environment": {}, - "instruments": {}, - "datasets": {} - } - - def add_project_info(self, key_or_dict, value=None, append=False): - self._add_info("project", key_or_dict, value, append) - - def add_sample_info(self, key_or_dict, value=None, append=False): - self._add_info("sample", key_or_dict, value, append) - - def add_environment_info(self, key_or_dict, value=None, append=False): - self._add_info("environment", key_or_dict, value, append) - - def add_instrument_info(self, key_or_dict, value=None, append=False): - self._add_info("instruments", key_or_dict, value, append) - - def add_dataset_info(self, key_or_dict, value=None, append=False): - self._add_info("datasets", key_or_dict, value, append) - - def _add_info(self, category, key_or_dict, value, append): - """Internal helper method to add information to a category.""" - if isinstance(key_or_dict, dict): - self.metadata[category].update(key_or_dict) - else: - if key_or_dict in self.metadata[category]: - if append: - current_value = self.metadata[category][key_or_dict] - - if isinstance(current_value, list): - - if not isinstance(value, list): - # Append the new value to the list - self.metadata[category][key_or_dict].append(value) - else: - self.metadata[category][key_or_dict] = current_value + value - - elif isinstance(current_value, str): - # Append the new value as a comma-separated string - self.metadata[category][key_or_dict] = current_value + ',' + str(value) - else: - # Handle other types (for completeness, usually not required) - self.metadata[category][key_or_dict] = [current_value, value] - else: - self.metadata[category][key_or_dict] = value - else: - self.metadata[category][key_or_dict] = value - - def get_metadata(self): - return { - "parent_files": self.parent_files, - "metadata": self.metadata - } - - def print_metadata(self): - print("parent_files", self.parent_files) - - for key in self.metadata.keys(): - print(key,'metadata:\n') - for item in self.metadata[key].items(): - print(item[0],item[1]) - - - - def clear_metadata(self): - self.metadata = { - "project": {}, - "sample": {}, - "environment": {}, - "instruments": {}, - "datasets": {} - } - self.parent_files = [] - -def main(): - - output_filename_path = "output_files/unified_file_smog_chamber_2024-03-19_UTC-OFST_+0100_NG.h5" - output_yml_filename_path = "output_files/unified_file_smog_chamber_2024-03-19_UTC-OFST_+0100_NG.yalm" - output_yml_filename_path_tail, filename = os.path.split(output_yml_filename_path) - #output_yml_filename_path = hdf5_ops.serialize_metadata(output_filename_path) - - #first_initialize_metadata_review(output_filename_path,initials='NG') - #second_submit_metadata_review() - #if os.path.exists(os.path.join(os.path.join(os.path.abspath(os.curdir),"review"),filename)): - # third_update_hdf5_file_with_review(output_filename_path, os.path.join(os.path.join(os.path.abspath(os.curdir),"review"),filename)) - #fourth_complete_metadata_review() +import subprocess +import os +import utils.g5505_utils as utils +from pipelines.metadata_revision import update_hdf5_file_with_review + +def perform_git_operations(hdf5_upload): + status_command = ['git', 'status'] + status = subprocess.run(status_command, capture_output=True, check=True) + + if hdf5_upload: + upload_ext = ['.h5', '.yaml'] + else: + upload_ext = ['.yaml'] + + files_to_add_list = extract_files_to_add(status.stdout, upload_ext) + if files_to_add_list: + add_files_to_git(files_to_add_list) + commit_changes('Updated hdf5 file with yaml review file.') + else: + print("There were no found h5 and yaml files, needing to be saved. This action will not have effect on the review process' commit history.") + +def extract_files_to_add(git_status_output, upload_ext): + files_to_add_list = [] + for line in git_status_output.splitlines(): + tmp = line.decode("utf-8") + if 'modified' in tmp: + if any(ext in tmp for ext in upload_ext): + files_to_add_list.append(tmp.split()[1]) + return files_to_add_list + +def add_files_to_git(files_to_add_list): + add_command = ['git', 'add'] + files_to_add_list + subprocess.run(add_command, capture_output=True, check=True) + +def commit_changes(message): + commit_command = ['git', 'commit', '-m', message] + commit_output = subprocess.run(commit_command, capture_output=True, check=True) + print(commit_output.stdout) + +def get_status(): + return subprocess.run(['git','status'],capture_output=True,text=True,check=True) + +def show_current_branch(): + current_branch_command = ['git','branch','--show-current'] + subprocess.run(current_branch_command,capture_output=True,text=True,check=True) + + + +YAML_EXT = ".yaml" +TXT_EXT = ".txt" + + + +def get_review_status(filename_path): + + filename_path_tail, filename_path_head = os.path.split(filename_path) + filename, ext = os.path.splitext(filename_path_head) + # TODO: + with open(os.path.join("review/",filename+"-review_status"+TXT_EXT),'r') as f: + workflow_steps = [] + for line in f: + workflow_steps.append(line) + return workflow_steps[-1] + +def first_initialize_metadata_review(hdf5_file_path, reviewer_attrs, restart = False): + + """ + First: Initialize review branch with review folder with a copy of yaml representation of + hdf5 file under review and by creating a txt file with the state of the review process, e.g., under review. + + """ + + initials = reviewer_attrs['initials'] + #branch_name = '-'.join([reviewer_attrs['type'],'review_',initials]) + branch_name = '_'.join(['review',initials]) + + hdf5_file_path_tail, filename_path_head = os.path.split(hdf5_file_path) + filename, ext = os.path.splitext(filename_path_head) + + # Check file_path points to h5 file + if not 'h5' in ext: + raise ValueError("filename_path needs to point to an h5 file.") + + # Verify if yaml snapshot of input h5 file exists + if not os.path.exists(os.path.join(hdf5_file_path_tail,filename+YAML_EXT)): + raise ValueError("metadata review cannot be initialized. The associated .yaml file under review was not found. Run serialize_metadata(filename_path) ") + + # Initialize metadata review workflow + # print("Create branch metadata-review-by-"+initials+"\n") + + #checkout_review_branch(branch_name) + + # Check you are working at the right branch + + curr_branch = show_current_branch() + if not branch_name in curr_branch.stdout: + raise ValueError("Branch "+branch_name+" was not found. \nPlease open a Git Bash Terminal, and follow the below instructions: \n1. Change directory to your project's directory. \n2. Excecute the command: git checkout "+branch_name) + + # Check if review file already exists and then check if it is still untracked + review_yaml_file_path = os.path.join("review/",filename+YAML_EXT) + review_yaml_file_path_tail, ext = os.path.splitext(review_yaml_file_path) + review_status_yaml_file_path = os.path.join(review_yaml_file_path_tail+"-review_status"+".txt") + + if not os.path.exists(review_yaml_file_path) or restart: + review_yaml_file_path = utils.make_file_copy(os.path.join(hdf5_file_path_tail,filename+YAML_EXT), 'review') + if restart: + print('metadata review has been reinitialized. The review files will reflect the current state of the hdf5 files metadata') + + + + #if not os.path.exists(os.path.join(review_yaml_file_path_tail+"-review_status"+".txt")): + + with open(review_status_yaml_file_path,'w') as f: + f.write('under review') + + # Stage untracked review files and commit them to local repository + status = get_status() + untracked_files = [] + for line in status.stdout.splitlines(): + #tmp = line.decode("utf-8") + #modified_files.append(tmp.split()[1]) + if 'review/' in line: + if not 'modified' in line: # untracked filesand + untracked_files.append(line.strip()) + else: + untracked_files.append(line.strip().split()[1]) + + if 'output_files/'+filename+YAML_EXT in line and not 'modified' in line: + untracked_files.append(line.strip()) + + if untracked_files: + result = subprocess.run(add_files_to_git(untracked_files),capture_output=True,check=True) + message = 'Initialized metadata review.' + commit_output = subprocess.run(commit_changes(message),capture_output=True,check=True) + + for line in commit_output.stdout.splitlines(): + print(line.decode('utf-8')) + #else: + # print('This action will not have any effect because metadata review process has been already initialized.') + + + + + #status_dict = repo_obj.status() + #for filepath, file_status in status_dict.items(): + # Identify keys associated to review files and stage them + # if 'review/'+filename in filepath: + # Stage changes + # repo_obj.index.add(filepath) + + #author = config_file.author #default_signature + #committer = config_file.committer + #message = "Initialized metadata review process." + #tree = repo_obj.index.write_tree() + #oid = repo_obj.create_commit('HEAD', author, committer, message, tree, [repo_obj.head.peel().oid]) + + #print("Add and commit"+"\n") + + return review_yaml_file_path, review_status_yaml_file_path + + + +def second_save_metadata_review(review_yaml_file_path, reviewer_attrs): + """ + Second: Once you're done reviewing the yaml representation of hdf5 file in review folder. + Change the review status to complete and save (add and commit) modified .yalm and .txt files in the project by + running this function. + + """ + # 1 verify review initializatin was performed first + # 2. change review status in txt to complete + # 3. git add review/ and git commit -m "Submitted metadata review" + + initials = reviewer_attrs['initials'] + #branch_name = '-'.join([reviewer_attrs['type'],'review','by',initials]) + branch_name = '_'.join(['review',initials]) + # TODO: replace with subprocess + git + #checkout_review_branch(repo_obj, branch_name) + + # Check you are working at the right branch + curr_branch = show_current_branch() + if not branch_name in curr_branch.stdout: + raise ValueError('Please checkout ' + branch_name + ' via Git Bash before submitting metadata review files. ') + + # Collect modified review files + status = get_status() + modified_files = [] + os.path.basename(review_yaml_file_path) + for line in status.stdout.splitlines(): + # conver line from bytes to str + tmp = line.decode("utf-8") + if 'modified' in tmp and 'review/' in tmp and os.path.basename(review_yaml_file_path) in tmp: + modified_files.append(tmp.split()[1]) + + # Stage modified files and commit them to local repository + review_yaml_file_path_tail, review_yaml_file_path_head = os.path.split(review_yaml_file_path) + filename, ext = os.path.splitext(review_yaml_file_path_head) + if modified_files: + review_status_file_path = os.path.join("review/",filename+"-review_status"+TXT_EXT) + with open(review_status_file_path,'a') as f: + f.write('\nsubmitted') + + modified_files.append(review_status_file_path) + + result = subprocess.run(add_files_to_git(modified_files),capture_output=True,check=True) + message = 'Submitted metadata review.' + commit_output = subprocess.run(commit_changes(message),capture_output=True,check=True) + + for line in commit_output.stdout.splitlines(): + print(line.decode('utf-8')) + else: + print('Nothing to commit.') + +# +def third_update_hdf5_file_with_review(input_hdf5_file, yaml_review_file, reviewer_attrs={}, hdf5_upload=False): + if 'submitted' not in get_review_status(input_hdf5_file): + raise ValueError('Review yaml file must be submitted before trying to perform an update. Run first second_submit_metadata_review().') + + update_hdf5_file_with_review(input_hdf5_file, yaml_review_file) + perform_git_operations(hdf5_upload) + +def last_submit_metadata_review(reviewer_attrs): + + """Fourth: """ + + initials =reviewer_attrs['initials'] + + repository = 'origin' + branch_name = '_'.join(['review',initials]) + + push_command = lambda repository,refspec: ['git','push',repository,refspec] + + list_branches_command = ['git','branch','--list'] + + branches = subprocess.run(list_branches_command,capture_output=True,text=True,check=True) + if not branch_name in branches.stdout: + print('There is no branch named '+branch_name+'.\n') + print('Make sure to run data owner review workflow from the beginning without missing any steps.') + return + + curr_branch = show_current_branch() + if not branch_name in curr_branch.stdout: + print('Complete metadata review could not be completed.\n') + print('Make sure a data-owner workflow has already been started on branch '+branch_name+'\n') + print('The step "Complete metadata review" will have no effect.') + return + + + + # push + result = subprocess.run(push_command(repository,branch_name),capture_output=True,text=True,check=True) + print(result.stdout) + + # 1. git add output_files/ + # 2. delete review/ + #shutil.rmtree(os.path.join(os.path.abspath(os.curdir),"review")) + # 3. git rm review/ + # 4. git commit -m "Completed review process. Current state of hdf5 file and yml should be up to date." + return result.returncode + + +#import config_file +#import hdf5_ops + +class MetadataHarvester: + def __init__(self, parent_files=None): + if parent_files is None: + parent_files = [] + self.parent_files = parent_files + self.metadata = { + "project": {}, + "sample": {}, + "environment": {}, + "instruments": {}, + "datasets": {} + } + + def add_project_info(self, key_or_dict, value=None, append=False): + self._add_info("project", key_or_dict, value, append) + + def add_sample_info(self, key_or_dict, value=None, append=False): + self._add_info("sample", key_or_dict, value, append) + + def add_environment_info(self, key_or_dict, value=None, append=False): + self._add_info("environment", key_or_dict, value, append) + + def add_instrument_info(self, key_or_dict, value=None, append=False): + self._add_info("instruments", key_or_dict, value, append) + + def add_dataset_info(self, key_or_dict, value=None, append=False): + self._add_info("datasets", key_or_dict, value, append) + + def _add_info(self, category, key_or_dict, value, append): + """Internal helper method to add information to a category.""" + if isinstance(key_or_dict, dict): + self.metadata[category].update(key_or_dict) + else: + if key_or_dict in self.metadata[category]: + if append: + current_value = self.metadata[category][key_or_dict] + + if isinstance(current_value, list): + + if not isinstance(value, list): + # Append the new value to the list + self.metadata[category][key_or_dict].append(value) + else: + self.metadata[category][key_or_dict] = current_value + value + + elif isinstance(current_value, str): + # Append the new value as a comma-separated string + self.metadata[category][key_or_dict] = current_value + ',' + str(value) + else: + # Handle other types (for completeness, usually not required) + self.metadata[category][key_or_dict] = [current_value, value] + else: + self.metadata[category][key_or_dict] = value + else: + self.metadata[category][key_or_dict] = value + + def get_metadata(self): + return { + "parent_files": self.parent_files, + "metadata": self.metadata + } + + def print_metadata(self): + print("parent_files", self.parent_files) + + for key in self.metadata.keys(): + print(key,'metadata:\n') + for item in self.metadata[key].items(): + print(item[0],item[1]) + + + + def clear_metadata(self): + self.metadata = { + "project": {}, + "sample": {}, + "environment": {}, + "instruments": {}, + "datasets": {} + } + self.parent_files = [] + +def main(): + + output_filename_path = "output_files/unified_file_smog_chamber_2024-03-19_UTC-OFST_+0100_NG.h5" + output_yml_filename_path = "output_files/unified_file_smog_chamber_2024-03-19_UTC-OFST_+0100_NG.yalm" + output_yml_filename_path_tail, filename = os.path.split(output_yml_filename_path) + #output_yml_filename_path = hdf5_ops.serialize_metadata(output_filename_path) + + #first_initialize_metadata_review(output_filename_path,initials='NG') + #second_submit_metadata_review() + #if os.path.exists(os.path.join(os.path.join(os.path.abspath(os.curdir),"review"),filename)): + # third_update_hdf5_file_with_review(output_filename_path, os.path.join(os.path.join(os.path.abspath(os.curdir),"review"),filename)) + #fourth_complete_metadata_review() diff --git a/src/hdf5_ops.py b/src/hdf5_ops.py index 7b1003b..5ee2730 100644 --- a/src/hdf5_ops.py +++ b/src/hdf5_ops.py @@ -1,674 +1,674 @@ -import sys -import os - -try: - thisFilePath = os.path.abspath(__file__) -except NameError: - print("Error: __file__ is not available. Ensure the script is being run from a file.") - print("[Notice] Path to DIMA package may not be resolved properly.") - thisFilePath = os.getcwd() # Use current directory or specify a default - -dimaPath = os.path.normpath(os.path.join(thisFilePath, "..",'..')) # Move up to project root - -if dimaPath not in sys.path: # Avoid duplicate entries - sys.path.append(dimaPath) - - -import h5py -import pandas as pd -import numpy as np - -import utils.g5505_utils as utils -import src.hdf5_writer as hdf5_lib -import logging -import datetime - -import h5py - -import yaml -import json -import copy - -class HDF5DataOpsManager(): - - """ - A class to handle HDF5 fundamental middle level file operations to power data updates, metadata revision, and data analysis - with hdf5 files encoding multi-instrument experimental campaign data. - - Parameters: - ----------- - path_to_file : str - path/to/hdf5file. - mode : str - 'r' or 'r+' read or read/write mode only when file exists - """ - def __init__(self, file_path, mode = 'r+') -> None: - - # Class attributes - if mode in ['r','r+']: - self.mode = mode - self.file_path = file_path - self.file_obj = None - #self._open_file() - self.dataset_metadata_df = None - - # Define private methods - - # Define public methods - - def load_file_obj(self): - if self.file_obj is None: - self.file_obj = h5py.File(self.file_path, self.mode) - - def unload_file_obj(self): - if self.file_obj: - self.file_obj.flush() # Ensure all data is written to disk - self.file_obj.close() - self.file_obj = None - self.dataset_metadata_df = None # maybe replace by del self.dataset_metadata_df to explicitly clear the reference as well as the memory. - - def extract_and_load_dataset_metadata(self): - - def __get_datasets(name, obj, list_of_datasets): - if isinstance(obj,h5py.Dataset): - list_of_datasets.append(name) - #print(f'Adding dataset: {name}') #tail: {head} head: {tail}') - list_of_datasets = [] - - if self.file_obj is None: - raise RuntimeError("File object is not loaded. Please load the HDF5 file using the 'load_file_obj' method before attempting to extract datasets.") - - try: - - list_of_datasets = [] - - self.file_obj.visititems(lambda name, obj: __get_datasets(name, obj, list_of_datasets)) - - dataset_metadata_df = pd.DataFrame({'dataset_name': list_of_datasets}) - dataset_metadata_df['parent_instrument'] = dataset_metadata_df['dataset_name'].apply(lambda x: '/'.join(x.split('/')[i] for i in range(0,len(x.split('/')) - 2)))#[-3])) - dataset_metadata_df['parent_file'] = dataset_metadata_df['dataset_name'].apply(lambda x: x.split('/')[-2]) - - self.dataset_metadata_df = dataset_metadata_df - - except Exception as e: - - self.unload_file_obj() - print(f"An unexpected error occurred: {e}. File object will be unloaded.") - - - - - def extract_dataset_as_dataframe(self,dataset_name): - """ - returns a copy of the dataset content in the form of dataframe when possible or numpy array - """ - if self.file_obj is None: - raise RuntimeError("File object is not loaded. Please load the HDF5 file using the 'load_file_obj' method before attempting to extract datasets.") - - dataset_obj = self.file_obj[dataset_name] - # Read dataset content from dataset obj - data = dataset_obj[...] - # The above statement can be understood as follows: - # data = np.empty(shape=dataset_obj.shape, - # dtype=dataset_obj.dtype) - # dataset_obj.read_direct(data) - - try: - return pd.DataFrame(data) - except ValueError as e: - logging.error(f"Failed to convert dataset '{dataset_name}' to DataFrame: {e}. Instead, dataset will be returned as Numpy array.") - return data # 'data' is a NumPy array here - except Exception as e: - self.unload_file_obj() - print(f"An unexpected error occurred: {e}. Returning None and unloading file object") - return None - - # Define metadata revision methods: append(), update(), delete(), and rename(). - - def append_metadata(self, obj_name, annotation_dict): - """ - Appends metadata attributes to the specified object (obj_name) based on the provided annotation_dict. - - This method ensures that the provided metadata attributes do not overwrite any existing ones. If an attribute already exists, - a ValueError is raised. The function supports storing scalar values (int, float, str) and compound values such as dictionaries - that are converted into NumPy structured arrays before being added to the metadata. - - Parameters: - ----------- - obj_name: str - Path to the target object (dataset or group) within the HDF5 file. - - annotation_dict: dict - A dictionary where the keys represent new attribute names (strings), and the values can be: - - Scalars: int, float, or str. - - Compound values (dictionaries) for more complex metadata, which are converted to NumPy structured arrays. - Example of a compound value: - - Example: - ---------- - annotation_dict = { - "relative_humidity": { - "value": 65, - "units": "percentage", - "range": "[0,100]", - "definition": "amount of water vapor present ..." - } - } - """ - - if self.file_obj is None: - raise RuntimeError("File object is not loaded. Please load the HDF5 file using the 'load_file_obj' method before attempting to modify it.") - - # Create a copy of annotation_dict to avoid modifying the original - annotation_dict_copy = copy.deepcopy(annotation_dict) - - try: - obj = self.file_obj[obj_name] - - # Check if any attribute already exists - if any(key in obj.attrs for key in annotation_dict_copy.keys()): - raise ValueError("Make sure the provided (key, value) pairs are not existing metadata elements or attributes. To modify or delete existing attributes use .modify_annotation() or .delete_annotation()") - - # Process the dictionary values and convert them to structured arrays if needed - for key, value in annotation_dict_copy.items(): - if isinstance(value, dict): - # Convert dictionaries to NumPy structured arrays for complex attributes - annotation_dict_copy[key] = utils.convert_attrdict_to_np_structured_array(value) - - # Update the object's attributes with the new metadata - obj.attrs.update(annotation_dict_copy) - - except Exception as e: - self.unload_file_obj() - print(f"An unexpected error occurred: {e}. The file object has been properly closed.") - - - def update_metadata(self, obj_name, annotation_dict): - """ - Updates the value of existing metadata attributes of the specified object (obj_name) based on the provided annotation_dict. - - The function disregards non-existing attributes and suggests to use the append_metadata() method to include those in the metadata. - - Parameters: - ----------- - obj_name : str - Path to the target object (dataset or group) within the HDF5 file. - - annotation_dict: dict - A dictionary where the keys represent existing attribute names (strings), and the values can be: - - Scalars: int, float, or str. - - Compound values (dictionaries) for more complex metadata, which are converted to NumPy structured arrays. - Example of a compound value: - - Example: - ---------- - annotation_dict = { - "relative_humidity": { - "value": 65, - "units": "percentage", - "range": "[0,100]", - "definition": "amount of water vapor present ..." - } - } - - - """ - - if self.file_obj is None: - raise RuntimeError("File object is not loaded. Please load the HDF5 file using the 'load_file_obj' method before attempting to modify it.") - - update_dict = {} - - try: - - obj = self.file_obj[obj_name] - for key, value in annotation_dict.items(): - if key in obj.attrs: - if isinstance(value, dict): - update_dict[key] = utils.convert_attrdict_to_np_structured_array(value) - else: - update_dict[key] = value - else: - # Optionally, log or warn about non-existing keys being ignored. - print(f"Warning: Key '{key}' does not exist and will be ignored.") - - obj.attrs.update(update_dict) - - except Exception as e: - self.unload_file_obj() - print(f"An unexpected error occurred: {e}. The file object has been properly closed.") - - def delete_metadata(self, obj_name, annotation_dict): - """ - Deletes metadata attributes of the specified object (obj_name) based on the provided annotation_dict. - - Parameters: - ----------- - obj_name: str - Path to the target object (dataset or group) within the HDF5 file. - - annotation_dict: dict - Dictionary where keys represent attribute names, and values should be dictionaries containing - {"delete": True} to mark them for deletion. - - Example: - -------- - annotation_dict = {"attr_to_be_deleted": {"delete": True}} - - Behavior: - --------- - - Deletes the specified attributes from the object's metadata if marked for deletion. - - Issues a warning if the attribute is not found or not marked for deletion. - """ - - if self.file_obj is None: - raise RuntimeError("File object is not loaded. Please load the HDF5 file using the 'load_file_obj' method before attempting to modify it.") - - try: - obj = self.file_obj[obj_name] - for attr_key, value in annotation_dict.items(): - if attr_key in obj.attrs: - if isinstance(value, dict) and value.get('delete', False): - obj.attrs.__delitem__(attr_key) - else: - msg = f"Warning: Value for key '{attr_key}' is not marked for deletion or is invalid." - print(msg) - else: - msg = f"Warning: Key '{attr_key}' does not exist in metadata." - print(msg) - - except Exception as e: - self.unload_file_obj() - print(f"An unexpected error occurred: {e}. The file object has been properly closed.") - - - def rename_metadata(self, obj_name, renaming_map): - """ - Renames metadata attributes of the specified object (obj_name) based on the provided renaming_map. - - Parameters: - ----------- - obj_name: str - Path to the target object (dataset or group) within the HDF5 file. - - renaming_map: dict - A dictionary where keys are current attribute names (strings), and values are the new attribute names (strings or byte strings) to rename to. - - Example: - -------- - renaming_map = { - "old_attr_name": "new_attr_name", - "old_attr_2": "new_attr_2" - } - - """ - - if self.file_obj is None: - raise RuntimeError("File object is not loaded. Please load the HDF5 file using the 'load_file_obj' method before attempting to modify it.") - - try: - obj = self.file_obj[obj_name] - # Iterate over the renaming_map to process renaming - for old_attr, new_attr in renaming_map.items(): - if old_attr in obj.attrs: - # Get the old attribute's value - attr_value = obj.attrs[old_attr] - - # Create a new attribute with the new name - obj.attrs.create(new_attr, data=attr_value) - - # Delete the old attribute - obj.attrs.__delitem__(old_attr) - else: - # Skip if the old attribute doesn't exist - msg = f"Skipping: Attribute '{old_attr}' does not exist." - print(msg) # Optionally, replace with warnings.warn(msg) - except Exception as e: - self.unload_file_obj() - print( - f"An unexpected error occurred: {e}. The file object has been properly closed. " - "Please ensure that 'obj_name' exists in the file, and that the keys in 'renaming_map' are valid attributes of the object." - ) - - self.unload_file_obj() - - def get_metadata(self, obj_path): - """ Get file attributes from object at path = obj_path. For example, - obj_path = '/' will get root level attributes or metadata. - """ - try: - # Access the attributes for the object at the given path - metadata_dict = self.file_obj[obj_path].attrs - except KeyError: - # Handle the case where the path doesn't exist - logging.error(f'Invalid object path: {obj_path}') - metadata_dict = {} - - return metadata_dict - - - def reformat_datetime_column(self, dataset_name, column_name, src_format, desired_format='%Y-%m-%d %H:%M:%S.%f'): - # Access the dataset - dataset = self.file_obj[dataset_name] - - # Read the column data into a pandas Series and decode bytes to strings - dt_column_data = pd.Series(dataset[column_name][:]).apply(lambda x: x.decode() ) - - # Convert to datetime using the source format - dt_column_data = pd.to_datetime(dt_column_data, format=src_format, errors = 'coerce') - - # Reformat datetime objects to the desired format as strings - dt_column_data = dt_column_data.dt.strftime(desired_format) - - # Encode the strings back to bytes - #encoded_data = dt_column_data.apply(lambda x: x.encode() if not pd.isnull(x) else 'N/A').to_numpy() - - # Update the dataset in place - #dataset[column_name][:] = encoded_data - - # Convert byte strings to datetime objects - #timestamps = [datetime.datetime.strptime(a.decode(), src_format).strftime(desired_format) for a in dt_column_data] - - #datetime.strptime('31/01/22 23:59:59.999999', - # '%d/%m/%y %H:%M:%S.%f') - - #pd.to_datetime( - # np.array([a.decode() for a in dt_column_data]), - # format=src_format, - # errors='coerce' - #) - - - # Standardize the datetime format - #standardized_time = datetime.strftime(desired_format) - - # Convert to byte strings to store back in the HDF5 dataset - #standardized_time_bytes = np.array([s.encode() for s in timestamps]) - - # Update the column in the dataset (in-place update) - # TODO: make this a more secure operation - #dataset[column_name][:] = standardized_time_bytes - - #return np.array(timestamps) - return dt_column_data.to_numpy() - - # Define data append operations: append_dataset(), and update_file() - - def append_dataset(self,dataset_dict, group_name): - - # Parse value into HDF5 admissible type - for key in dataset_dict['attributes'].keys(): - value = dataset_dict['attributes'][key] - if isinstance(value, dict): - dataset_dict['attributes'][key] = utils.convert_attrdict_to_np_structured_array(value) - - if not group_name in self.file_obj: - self.file_obj.create_group(group_name, track_order=True) - self.file_obj[group_name].attrs['creation_date'] = utils.created_at().encode("utf-8") - - self.file_obj[group_name].create_dataset(dataset_dict['name'], data=dataset_dict['data']) - self.file_obj[group_name][dataset_dict['name']].attrs.update(dataset_dict['attributes']) - self.file_obj[group_name].attrs['last_update_date'] = utils.created_at().encode("utf-8") - - def update_file(self, path_to_append_dir): - # Split the reference file path and the append directory path into directories and filenames - ref_tail, ref_head = os.path.split(self.file_path) - ref_head_filename, head_ext = os.path.splitext(ref_head) - tail, head = os.path.split(path_to_append_dir) - - - # Ensure the append directory is in the same directory as the reference file and has the same name (without extension) - if not (ref_tail == tail and ref_head_filename == head): - raise ValueError("The append directory must be in the same directory as the reference HDF5 file and have the same name without the extension.") - - # Close the file if it's already open - if self.file_obj is not None: - self.unload_file_obj() - - # Attempt to open the file in 'r+' mode for appending - try: - hdf5_lib.create_hdf5_file_from_filesystem_path(path_to_append_dir, mode='r+') - except FileNotFoundError: - raise FileNotFoundError(f"Reference HDF5 file '{self.file_path}' not found.") - except OSError as e: - raise OSError(f"Error opening HDF5 file: {e}") - - - -def get_parent_child_relationships(file: h5py.File): - - nodes = ['/'] - parent = [''] - #values = [file.attrs['count']] - # TODO: maybe we should make this more general and not dependent on file_list attribute? - #if 'file_list' in file.attrs.keys(): - # values = [len(file.attrs['file_list'])] - #else: - # values = [1] - values = [len(file.keys())] - - def node_visitor(name,obj): - if name.count('/') <=2: - nodes.append(obj.name) - parent.append(obj.parent.name) - #nodes.append(os.path.split(obj.name)[1]) - #parent.append(os.path.split(obj.parent.name)[1]) - - if isinstance(obj,h5py.Dataset):# or not 'file_list' in obj.attrs.keys(): - values.append(1) - else: - print(obj.name) - try: - values.append(len(obj.keys())) - except: - values.append(0) - - file.visititems(node_visitor) - - return nodes, parent, values - - -def __print_metadata__(name, obj, folder_depth, yaml_dict): - - """ - Extracts metadata from HDF5 groups and datasets and organizes them into a dictionary with compact representation. - - Parameters: - ----------- - name (str): Name of the HDF5 object being inspected. - obj (h5py.Group or h5py.Dataset): The HDF5 object (Group or Dataset). - folder_depth (int): Maximum depth of folders to explore. - yaml_dict (dict): Dictionary to populate with metadata. - """ - # Process only objects within the specified folder depth - if len(obj.name.split('/')) <= folder_depth: # and ".h5" not in obj.name: - name_to_list = obj.name.split('/') - name_head = name_to_list[-1] if not name_to_list[-1]=='' else obj.name - - if isinstance(obj, h5py.Group): # Handle groups - # Convert attributes to a YAML/JSON serializable format - attr_dict = {key: utils.to_serializable_dtype(val) for key, val in obj.attrs.items()} - - # Initialize the group dictionary - group_dict = {"name": name_head, "attributes": attr_dict} - - # Handle group members compactly - #subgroups = [member_name for member_name in obj if isinstance(obj[member_name], h5py.Group)] - #datasets = [member_name for member_name in obj if isinstance(obj[member_name], h5py.Dataset)] - - # Summarize groups and datasets - #group_dict["content_summary"] = { - # "group_count": len(subgroups), - # "group_preview": subgroups[:3] + (["..."] if len(subgroups) > 3 else []), - # "dataset_count": len(datasets), - # "dataset_preview": datasets[:3] + (["..."] if len(datasets) > 3 else []) - #} - - yaml_dict[obj.name] = group_dict - - elif isinstance(obj, h5py.Dataset): # Handle datasets - # Convert attributes to a YAML/JSON serializable format - attr_dict = {key: utils.to_serializable_dtype(val) for key, val in obj.attrs.items()} - - dataset_dict = {"name": name_head, "attributes": attr_dict} - - yaml_dict[obj.name] = dataset_dict - - - -def serialize_metadata(input_filename_path, folder_depth: int = 4, output_format: str = 'yaml') -> str: - """ - Serialize metadata from an HDF5 file into YAML or JSON format. - - Parameters - ---------- - input_filename_path : str - The path to the input HDF5 file. - folder_depth : int, optional - The folder depth to control how much of the HDF5 file hierarchy is traversed (default is 4). - output_format : str, optional - The format to serialize the output, either 'yaml' or 'json' (default is 'yaml'). - - Returns - ------- - str - The output file path where the serialized metadata is stored (either .yaml or .json). - - """ - - # Choose the appropriate output format (YAML or JSON) - if output_format not in ['yaml', 'json']: - raise ValueError("Unsupported format. Please choose either 'yaml' or 'json'.") - - # Initialize dictionary to store YAML/JSON data - yaml_dict = {} - - # Split input file path to get the output file's base name - output_filename_tail, ext = os.path.splitext(input_filename_path) - - # Open the HDF5 file and extract metadata - with h5py.File(input_filename_path, 'r') as f: - # Convert attribute dict to a YAML/JSON serializable dict - #attrs_dict = {key: utils.to_serializable_dtype(val) for key, val in f.attrs.items()} - #yaml_dict[f.name] = { - # "name": f.name, - # "attributes": attrs_dict, - # "datasets": {} - #} - __print_metadata__(f.name, f, folder_depth, yaml_dict) - # Traverse HDF5 file hierarchy and add datasets - f.visititems(lambda name, obj: __print_metadata__(name, obj, folder_depth, yaml_dict)) - - - # Serialize and write the data - output_file_path = output_filename_tail + '.' + output_format - with open(output_file_path, 'w') as output_file: - if output_format == 'json': - json_output = json.dumps(yaml_dict, indent=4, sort_keys=False) - output_file.write(json_output) - elif output_format == 'yaml': - yaml_output = yaml.dump(yaml_dict, sort_keys=False) - output_file.write(yaml_output) - - return output_file_path - - -def get_groups_at_a_level(file: h5py.File, level: str): - - groups = [] - def node_selector(name, obj): - if name.count('/') == level: - print(name) - groups.append(obj.name) - - file.visititems(node_selector) - #file.visititems() - return groups - -def read_mtable_as_dataframe(filename): - - """ - Reconstruct a MATLAB Table encoded in a .h5 file as a Pandas DataFrame. - - This function reads a .h5 file containing a MATLAB Table and reconstructs it as a Pandas DataFrame. - The input .h5 file contains one group per row of the MATLAB Table. Each group stores the table's - dataset-like variables as Datasets, while categorical and numerical variables are represented as - attributes of the respective group. - - To ensure homogeneity of data columns, the DataFrame is constructed column-wise. - - Parameters - ---------- - filename : str - The name of the .h5 file. This may include the file's location and path information. - - Returns - ------- - pd.DataFrame - The MATLAB Table reconstructed as a Pandas DataFrame. - """ - - - #contructs dataframe by filling out entries columnwise. This way we can ensure homogenous data columns""" - - with h5py.File(filename,'r') as file: - - # Define group's attributes and datasets. This should hold - # for all groups. TODO: implement verification and noncompliance error if needed. - group_list = list(file.keys()) - group_attrs = list(file[group_list[0]].attrs.keys()) - # - column_attr_names = [item[item.find('_')+1::] for item in group_attrs] - column_attr_names_idx = [int(item[4:(item.find('_'))]) for item in group_attrs] - - group_datasets = list(file[group_list[0]].keys()) if not 'DS_EMPTY' in file[group_list[0]].keys() else [] - # - column_dataset_names = [file[group_list[0]][item].attrs['column_name'] for item in group_datasets] - column_dataset_names_idx = [int(item[2:]) for item in group_datasets] - - - # Define data_frame as group_attrs + group_datasets - #pd_series_index = group_attrs + group_datasets - pd_series_index = column_attr_names + column_dataset_names - - output_dataframe = pd.DataFrame(columns=pd_series_index,index=group_list) - - tmp_col = [] - - for meas_prop in group_attrs + group_datasets: - if meas_prop in group_attrs: - column_label = meas_prop[meas_prop.find('_')+1:] - # Create numerical or categorical column from group's attributes - tmp_col = [file[group_key].attrs[meas_prop][()][0] for group_key in group_list] - else: - # Create dataset column from group's datasets - column_label = file[group_list[0] + '/' + meas_prop].attrs['column_name'] - #tmp_col = [file[group_key + '/' + meas_prop][()][0] for group_key in group_list] - tmp_col = [file[group_key + '/' + meas_prop][()] for group_key in group_list] - - output_dataframe.loc[:,column_label] = tmp_col - - return output_dataframe - -if __name__ == "__main__": - if len(sys.argv) < 5: - print("Usage: python hdf5_ops.py serialize ") - sys.exit(1) - - if sys.argv[1] == 'serialize': - input_hdf5_file = sys.argv[2] - folder_depth = int(sys.argv[3]) - file_format = sys.argv[4] - - try: - # Call the serialize_metadata function and capture the output path - path_to_file = serialize_metadata(input_hdf5_file, - folder_depth = folder_depth, - output_format=file_format) - print(f"Metadata serialized to {path_to_file}") - except Exception as e: - print(f"An error occurred during serialization: {e}") - sys.exit(1) - - #run(sys.argv[2]) - +import sys +import os + +try: + thisFilePath = os.path.abspath(__file__) +except NameError: + print("Error: __file__ is not available. Ensure the script is being run from a file.") + print("[Notice] Path to DIMA package may not be resolved properly.") + thisFilePath = os.getcwd() # Use current directory or specify a default + +dimaPath = os.path.normpath(os.path.join(thisFilePath, "..",'..')) # Move up to project root + +if dimaPath not in sys.path: # Avoid duplicate entries + sys.path.append(dimaPath) + + +import h5py +import pandas as pd +import numpy as np + +import utils.g5505_utils as utils +import src.hdf5_writer as hdf5_lib +import logging +import datetime + +import h5py + +import yaml +import json +import copy + +class HDF5DataOpsManager(): + + """ + A class to handle HDF5 fundamental middle level file operations to power data updates, metadata revision, and data analysis + with hdf5 files encoding multi-instrument experimental campaign data. + + Parameters: + ----------- + path_to_file : str + path/to/hdf5file. + mode : str + 'r' or 'r+' read or read/write mode only when file exists + """ + def __init__(self, file_path, mode = 'r+') -> None: + + # Class attributes + if mode in ['r','r+']: + self.mode = mode + self.file_path = file_path + self.file_obj = None + #self._open_file() + self.dataset_metadata_df = None + + # Define private methods + + # Define public methods + + def load_file_obj(self): + if self.file_obj is None: + self.file_obj = h5py.File(self.file_path, self.mode) + + def unload_file_obj(self): + if self.file_obj: + self.file_obj.flush() # Ensure all data is written to disk + self.file_obj.close() + self.file_obj = None + self.dataset_metadata_df = None # maybe replace by del self.dataset_metadata_df to explicitly clear the reference as well as the memory. + + def extract_and_load_dataset_metadata(self): + + def __get_datasets(name, obj, list_of_datasets): + if isinstance(obj,h5py.Dataset): + list_of_datasets.append(name) + #print(f'Adding dataset: {name}') #tail: {head} head: {tail}') + list_of_datasets = [] + + if self.file_obj is None: + raise RuntimeError("File object is not loaded. Please load the HDF5 file using the 'load_file_obj' method before attempting to extract datasets.") + + try: + + list_of_datasets = [] + + self.file_obj.visititems(lambda name, obj: __get_datasets(name, obj, list_of_datasets)) + + dataset_metadata_df = pd.DataFrame({'dataset_name': list_of_datasets}) + dataset_metadata_df['parent_instrument'] = dataset_metadata_df['dataset_name'].apply(lambda x: '/'.join(x.split('/')[i] for i in range(0,len(x.split('/')) - 2)))#[-3])) + dataset_metadata_df['parent_file'] = dataset_metadata_df['dataset_name'].apply(lambda x: x.split('/')[-2]) + + self.dataset_metadata_df = dataset_metadata_df + + except Exception as e: + + self.unload_file_obj() + print(f"An unexpected error occurred: {e}. File object will be unloaded.") + + + + + def extract_dataset_as_dataframe(self,dataset_name): + """ + returns a copy of the dataset content in the form of dataframe when possible or numpy array + """ + if self.file_obj is None: + raise RuntimeError("File object is not loaded. Please load the HDF5 file using the 'load_file_obj' method before attempting to extract datasets.") + + dataset_obj = self.file_obj[dataset_name] + # Read dataset content from dataset obj + data = dataset_obj[...] + # The above statement can be understood as follows: + # data = np.empty(shape=dataset_obj.shape, + # dtype=dataset_obj.dtype) + # dataset_obj.read_direct(data) + + try: + return pd.DataFrame(data) + except ValueError as e: + logging.error(f"Failed to convert dataset '{dataset_name}' to DataFrame: {e}. Instead, dataset will be returned as Numpy array.") + return data # 'data' is a NumPy array here + except Exception as e: + self.unload_file_obj() + print(f"An unexpected error occurred: {e}. Returning None and unloading file object") + return None + + # Define metadata revision methods: append(), update(), delete(), and rename(). + + def append_metadata(self, obj_name, annotation_dict): + """ + Appends metadata attributes to the specified object (obj_name) based on the provided annotation_dict. + + This method ensures that the provided metadata attributes do not overwrite any existing ones. If an attribute already exists, + a ValueError is raised. The function supports storing scalar values (int, float, str) and compound values such as dictionaries + that are converted into NumPy structured arrays before being added to the metadata. + + Parameters: + ----------- + obj_name: str + Path to the target object (dataset or group) within the HDF5 file. + + annotation_dict: dict + A dictionary where the keys represent new attribute names (strings), and the values can be: + - Scalars: int, float, or str. + - Compound values (dictionaries) for more complex metadata, which are converted to NumPy structured arrays. + Example of a compound value: + + Example: + ---------- + annotation_dict = { + "relative_humidity": { + "value": 65, + "units": "percentage", + "range": "[0,100]", + "definition": "amount of water vapor present ..." + } + } + """ + + if self.file_obj is None: + raise RuntimeError("File object is not loaded. Please load the HDF5 file using the 'load_file_obj' method before attempting to modify it.") + + # Create a copy of annotation_dict to avoid modifying the original + annotation_dict_copy = copy.deepcopy(annotation_dict) + + try: + obj = self.file_obj[obj_name] + + # Check if any attribute already exists + if any(key in obj.attrs for key in annotation_dict_copy.keys()): + raise ValueError("Make sure the provided (key, value) pairs are not existing metadata elements or attributes. To modify or delete existing attributes use .modify_annotation() or .delete_annotation()") + + # Process the dictionary values and convert them to structured arrays if needed + for key, value in annotation_dict_copy.items(): + if isinstance(value, dict): + # Convert dictionaries to NumPy structured arrays for complex attributes + annotation_dict_copy[key] = utils.convert_attrdict_to_np_structured_array(value) + + # Update the object's attributes with the new metadata + obj.attrs.update(annotation_dict_copy) + + except Exception as e: + self.unload_file_obj() + print(f"An unexpected error occurred: {e}. The file object has been properly closed.") + + + def update_metadata(self, obj_name, annotation_dict): + """ + Updates the value of existing metadata attributes of the specified object (obj_name) based on the provided annotation_dict. + + The function disregards non-existing attributes and suggests to use the append_metadata() method to include those in the metadata. + + Parameters: + ----------- + obj_name : str + Path to the target object (dataset or group) within the HDF5 file. + + annotation_dict: dict + A dictionary where the keys represent existing attribute names (strings), and the values can be: + - Scalars: int, float, or str. + - Compound values (dictionaries) for more complex metadata, which are converted to NumPy structured arrays. + Example of a compound value: + + Example: + ---------- + annotation_dict = { + "relative_humidity": { + "value": 65, + "units": "percentage", + "range": "[0,100]", + "definition": "amount of water vapor present ..." + } + } + + + """ + + if self.file_obj is None: + raise RuntimeError("File object is not loaded. Please load the HDF5 file using the 'load_file_obj' method before attempting to modify it.") + + update_dict = {} + + try: + + obj = self.file_obj[obj_name] + for key, value in annotation_dict.items(): + if key in obj.attrs: + if isinstance(value, dict): + update_dict[key] = utils.convert_attrdict_to_np_structured_array(value) + else: + update_dict[key] = value + else: + # Optionally, log or warn about non-existing keys being ignored. + print(f"Warning: Key '{key}' does not exist and will be ignored.") + + obj.attrs.update(update_dict) + + except Exception as e: + self.unload_file_obj() + print(f"An unexpected error occurred: {e}. The file object has been properly closed.") + + def delete_metadata(self, obj_name, annotation_dict): + """ + Deletes metadata attributes of the specified object (obj_name) based on the provided annotation_dict. + + Parameters: + ----------- + obj_name: str + Path to the target object (dataset or group) within the HDF5 file. + + annotation_dict: dict + Dictionary where keys represent attribute names, and values should be dictionaries containing + {"delete": True} to mark them for deletion. + + Example: + -------- + annotation_dict = {"attr_to_be_deleted": {"delete": True}} + + Behavior: + --------- + - Deletes the specified attributes from the object's metadata if marked for deletion. + - Issues a warning if the attribute is not found or not marked for deletion. + """ + + if self.file_obj is None: + raise RuntimeError("File object is not loaded. Please load the HDF5 file using the 'load_file_obj' method before attempting to modify it.") + + try: + obj = self.file_obj[obj_name] + for attr_key, value in annotation_dict.items(): + if attr_key in obj.attrs: + if isinstance(value, dict) and value.get('delete', False): + obj.attrs.__delitem__(attr_key) + else: + msg = f"Warning: Value for key '{attr_key}' is not marked for deletion or is invalid." + print(msg) + else: + msg = f"Warning: Key '{attr_key}' does not exist in metadata." + print(msg) + + except Exception as e: + self.unload_file_obj() + print(f"An unexpected error occurred: {e}. The file object has been properly closed.") + + + def rename_metadata(self, obj_name, renaming_map): + """ + Renames metadata attributes of the specified object (obj_name) based on the provided renaming_map. + + Parameters: + ----------- + obj_name: str + Path to the target object (dataset or group) within the HDF5 file. + + renaming_map: dict + A dictionary where keys are current attribute names (strings), and values are the new attribute names (strings or byte strings) to rename to. + + Example: + -------- + renaming_map = { + "old_attr_name": "new_attr_name", + "old_attr_2": "new_attr_2" + } + + """ + + if self.file_obj is None: + raise RuntimeError("File object is not loaded. Please load the HDF5 file using the 'load_file_obj' method before attempting to modify it.") + + try: + obj = self.file_obj[obj_name] + # Iterate over the renaming_map to process renaming + for old_attr, new_attr in renaming_map.items(): + if old_attr in obj.attrs: + # Get the old attribute's value + attr_value = obj.attrs[old_attr] + + # Create a new attribute with the new name + obj.attrs.create(new_attr, data=attr_value) + + # Delete the old attribute + obj.attrs.__delitem__(old_attr) + else: + # Skip if the old attribute doesn't exist + msg = f"Skipping: Attribute '{old_attr}' does not exist." + print(msg) # Optionally, replace with warnings.warn(msg) + except Exception as e: + self.unload_file_obj() + print( + f"An unexpected error occurred: {e}. The file object has been properly closed. " + "Please ensure that 'obj_name' exists in the file, and that the keys in 'renaming_map' are valid attributes of the object." + ) + + self.unload_file_obj() + + def get_metadata(self, obj_path): + """ Get file attributes from object at path = obj_path. For example, + obj_path = '/' will get root level attributes or metadata. + """ + try: + # Access the attributes for the object at the given path + metadata_dict = self.file_obj[obj_path].attrs + except KeyError: + # Handle the case where the path doesn't exist + logging.error(f'Invalid object path: {obj_path}') + metadata_dict = {} + + return metadata_dict + + + def reformat_datetime_column(self, dataset_name, column_name, src_format, desired_format='%Y-%m-%d %H:%M:%S.%f'): + # Access the dataset + dataset = self.file_obj[dataset_name] + + # Read the column data into a pandas Series and decode bytes to strings + dt_column_data = pd.Series(dataset[column_name][:]).apply(lambda x: x.decode() ) + + # Convert to datetime using the source format + dt_column_data = pd.to_datetime(dt_column_data, format=src_format, errors = 'coerce') + + # Reformat datetime objects to the desired format as strings + dt_column_data = dt_column_data.dt.strftime(desired_format) + + # Encode the strings back to bytes + #encoded_data = dt_column_data.apply(lambda x: x.encode() if not pd.isnull(x) else 'N/A').to_numpy() + + # Update the dataset in place + #dataset[column_name][:] = encoded_data + + # Convert byte strings to datetime objects + #timestamps = [datetime.datetime.strptime(a.decode(), src_format).strftime(desired_format) for a in dt_column_data] + + #datetime.strptime('31/01/22 23:59:59.999999', + # '%d/%m/%y %H:%M:%S.%f') + + #pd.to_datetime( + # np.array([a.decode() for a in dt_column_data]), + # format=src_format, + # errors='coerce' + #) + + + # Standardize the datetime format + #standardized_time = datetime.strftime(desired_format) + + # Convert to byte strings to store back in the HDF5 dataset + #standardized_time_bytes = np.array([s.encode() for s in timestamps]) + + # Update the column in the dataset (in-place update) + # TODO: make this a more secure operation + #dataset[column_name][:] = standardized_time_bytes + + #return np.array(timestamps) + return dt_column_data.to_numpy() + + # Define data append operations: append_dataset(), and update_file() + + def append_dataset(self,dataset_dict, group_name): + + # Parse value into HDF5 admissible type + for key in dataset_dict['attributes'].keys(): + value = dataset_dict['attributes'][key] + if isinstance(value, dict): + dataset_dict['attributes'][key] = utils.convert_attrdict_to_np_structured_array(value) + + if not group_name in self.file_obj: + self.file_obj.create_group(group_name, track_order=True) + self.file_obj[group_name].attrs['creation_date'] = utils.created_at().encode("utf-8") + + self.file_obj[group_name].create_dataset(dataset_dict['name'], data=dataset_dict['data']) + self.file_obj[group_name][dataset_dict['name']].attrs.update(dataset_dict['attributes']) + self.file_obj[group_name].attrs['last_update_date'] = utils.created_at().encode("utf-8") + + def update_file(self, path_to_append_dir): + # Split the reference file path and the append directory path into directories and filenames + ref_tail, ref_head = os.path.split(self.file_path) + ref_head_filename, head_ext = os.path.splitext(ref_head) + tail, head = os.path.split(path_to_append_dir) + + + # Ensure the append directory is in the same directory as the reference file and has the same name (without extension) + if not (ref_tail == tail and ref_head_filename == head): + raise ValueError("The append directory must be in the same directory as the reference HDF5 file and have the same name without the extension.") + + # Close the file if it's already open + if self.file_obj is not None: + self.unload_file_obj() + + # Attempt to open the file in 'r+' mode for appending + try: + hdf5_lib.create_hdf5_file_from_filesystem_path(path_to_append_dir, mode='r+') + except FileNotFoundError: + raise FileNotFoundError(f"Reference HDF5 file '{self.file_path}' not found.") + except OSError as e: + raise OSError(f"Error opening HDF5 file: {e}") + + + +def get_parent_child_relationships(file: h5py.File): + + nodes = ['/'] + parent = [''] + #values = [file.attrs['count']] + # TODO: maybe we should make this more general and not dependent on file_list attribute? + #if 'file_list' in file.attrs.keys(): + # values = [len(file.attrs['file_list'])] + #else: + # values = [1] + values = [len(file.keys())] + + def node_visitor(name,obj): + if name.count('/') <=2: + nodes.append(obj.name) + parent.append(obj.parent.name) + #nodes.append(os.path.split(obj.name)[1]) + #parent.append(os.path.split(obj.parent.name)[1]) + + if isinstance(obj,h5py.Dataset):# or not 'file_list' in obj.attrs.keys(): + values.append(1) + else: + print(obj.name) + try: + values.append(len(obj.keys())) + except: + values.append(0) + + file.visititems(node_visitor) + + return nodes, parent, values + + +def __print_metadata__(name, obj, folder_depth, yaml_dict): + + """ + Extracts metadata from HDF5 groups and datasets and organizes them into a dictionary with compact representation. + + Parameters: + ----------- + name (str): Name of the HDF5 object being inspected. + obj (h5py.Group or h5py.Dataset): The HDF5 object (Group or Dataset). + folder_depth (int): Maximum depth of folders to explore. + yaml_dict (dict): Dictionary to populate with metadata. + """ + # Process only objects within the specified folder depth + if len(obj.name.split('/')) <= folder_depth: # and ".h5" not in obj.name: + name_to_list = obj.name.split('/') + name_head = name_to_list[-1] if not name_to_list[-1]=='' else obj.name + + if isinstance(obj, h5py.Group): # Handle groups + # Convert attributes to a YAML/JSON serializable format + attr_dict = {key: utils.to_serializable_dtype(val) for key, val in obj.attrs.items()} + + # Initialize the group dictionary + group_dict = {"name": name_head, "attributes": attr_dict} + + # Handle group members compactly + #subgroups = [member_name for member_name in obj if isinstance(obj[member_name], h5py.Group)] + #datasets = [member_name for member_name in obj if isinstance(obj[member_name], h5py.Dataset)] + + # Summarize groups and datasets + #group_dict["content_summary"] = { + # "group_count": len(subgroups), + # "group_preview": subgroups[:3] + (["..."] if len(subgroups) > 3 else []), + # "dataset_count": len(datasets), + # "dataset_preview": datasets[:3] + (["..."] if len(datasets) > 3 else []) + #} + + yaml_dict[obj.name] = group_dict + + elif isinstance(obj, h5py.Dataset): # Handle datasets + # Convert attributes to a YAML/JSON serializable format + attr_dict = {key: utils.to_serializable_dtype(val) for key, val in obj.attrs.items()} + + dataset_dict = {"name": name_head, "attributes": attr_dict} + + yaml_dict[obj.name] = dataset_dict + + + +def serialize_metadata(input_filename_path, folder_depth: int = 4, output_format: str = 'yaml') -> str: + """ + Serialize metadata from an HDF5 file into YAML or JSON format. + + Parameters + ---------- + input_filename_path : str + The path to the input HDF5 file. + folder_depth : int, optional + The folder depth to control how much of the HDF5 file hierarchy is traversed (default is 4). + output_format : str, optional + The format to serialize the output, either 'yaml' or 'json' (default is 'yaml'). + + Returns + ------- + str + The output file path where the serialized metadata is stored (either .yaml or .json). + + """ + + # Choose the appropriate output format (YAML or JSON) + if output_format not in ['yaml', 'json']: + raise ValueError("Unsupported format. Please choose either 'yaml' or 'json'.") + + # Initialize dictionary to store YAML/JSON data + yaml_dict = {} + + # Split input file path to get the output file's base name + output_filename_tail, ext = os.path.splitext(input_filename_path) + + # Open the HDF5 file and extract metadata + with h5py.File(input_filename_path, 'r') as f: + # Convert attribute dict to a YAML/JSON serializable dict + #attrs_dict = {key: utils.to_serializable_dtype(val) for key, val in f.attrs.items()} + #yaml_dict[f.name] = { + # "name": f.name, + # "attributes": attrs_dict, + # "datasets": {} + #} + __print_metadata__(f.name, f, folder_depth, yaml_dict) + # Traverse HDF5 file hierarchy and add datasets + f.visititems(lambda name, obj: __print_metadata__(name, obj, folder_depth, yaml_dict)) + + + # Serialize and write the data + output_file_path = output_filename_tail + '.' + output_format + with open(output_file_path, 'w') as output_file: + if output_format == 'json': + json_output = json.dumps(yaml_dict, indent=4, sort_keys=False) + output_file.write(json_output) + elif output_format == 'yaml': + yaml_output = yaml.dump(yaml_dict, sort_keys=False) + output_file.write(yaml_output) + + return output_file_path + + +def get_groups_at_a_level(file: h5py.File, level: str): + + groups = [] + def node_selector(name, obj): + if name.count('/') == level: + print(name) + groups.append(obj.name) + + file.visititems(node_selector) + #file.visititems() + return groups + +def read_mtable_as_dataframe(filename): + + """ + Reconstruct a MATLAB Table encoded in a .h5 file as a Pandas DataFrame. + + This function reads a .h5 file containing a MATLAB Table and reconstructs it as a Pandas DataFrame. + The input .h5 file contains one group per row of the MATLAB Table. Each group stores the table's + dataset-like variables as Datasets, while categorical and numerical variables are represented as + attributes of the respective group. + + To ensure homogeneity of data columns, the DataFrame is constructed column-wise. + + Parameters + ---------- + filename : str + The name of the .h5 file. This may include the file's location and path information. + + Returns + ------- + pd.DataFrame + The MATLAB Table reconstructed as a Pandas DataFrame. + """ + + + #contructs dataframe by filling out entries columnwise. This way we can ensure homogenous data columns""" + + with h5py.File(filename,'r') as file: + + # Define group's attributes and datasets. This should hold + # for all groups. TODO: implement verification and noncompliance error if needed. + group_list = list(file.keys()) + group_attrs = list(file[group_list[0]].attrs.keys()) + # + column_attr_names = [item[item.find('_')+1::] for item in group_attrs] + column_attr_names_idx = [int(item[4:(item.find('_'))]) for item in group_attrs] + + group_datasets = list(file[group_list[0]].keys()) if not 'DS_EMPTY' in file[group_list[0]].keys() else [] + # + column_dataset_names = [file[group_list[0]][item].attrs['column_name'] for item in group_datasets] + column_dataset_names_idx = [int(item[2:]) for item in group_datasets] + + + # Define data_frame as group_attrs + group_datasets + #pd_series_index = group_attrs + group_datasets + pd_series_index = column_attr_names + column_dataset_names + + output_dataframe = pd.DataFrame(columns=pd_series_index,index=group_list) + + tmp_col = [] + + for meas_prop in group_attrs + group_datasets: + if meas_prop in group_attrs: + column_label = meas_prop[meas_prop.find('_')+1:] + # Create numerical or categorical column from group's attributes + tmp_col = [file[group_key].attrs[meas_prop][()][0] for group_key in group_list] + else: + # Create dataset column from group's datasets + column_label = file[group_list[0] + '/' + meas_prop].attrs['column_name'] + #tmp_col = [file[group_key + '/' + meas_prop][()][0] for group_key in group_list] + tmp_col = [file[group_key + '/' + meas_prop][()] for group_key in group_list] + + output_dataframe.loc[:,column_label] = tmp_col + + return output_dataframe + +if __name__ == "__main__": + if len(sys.argv) < 5: + print("Usage: python hdf5_ops.py serialize ") + sys.exit(1) + + if sys.argv[1] == 'serialize': + input_hdf5_file = sys.argv[2] + folder_depth = int(sys.argv[3]) + file_format = sys.argv[4] + + try: + # Call the serialize_metadata function and capture the output path + path_to_file = serialize_metadata(input_hdf5_file, + folder_depth = folder_depth, + output_format=file_format) + print(f"Metadata serialized to {path_to_file}") + except Exception as e: + print(f"An error occurred during serialization: {e}") + sys.exit(1) + + #run(sys.argv[2]) + diff --git a/src/hdf5_writer.py b/src/hdf5_writer.py index c727ffc..739562e 100644 --- a/src/hdf5_writer.py +++ b/src/hdf5_writer.py @@ -1,396 +1,396 @@ -import sys -import os -root_dir = os.path.abspath(os.curdir) -sys.path.append(root_dir) - -import pandas as pd -import numpy as np -import h5py -import logging - -import utils.g5505_utils as utils -import instruments.readers.filereader_registry as filereader_registry - - - -def __transfer_file_dict_to_hdf5(h5file, group_name, file_dict): - """ - Transfers data from a file_dict to an HDF5 file. - - Parameters - ---------- - h5file : h5py.File - HDF5 file object where the data will be written. - group_name : str - Name of the HDF5 group where data will be stored. - file_dict : dict - Dictionary containing file data to be transferred. Required structure: - { - 'name': str, - 'attributes_dict': dict, - 'datasets': [ - { - 'name': str, - 'data': array-like, - 'shape': tuple, - 'attributes': dict (optional) - }, - ... - ] - } - - Returns - ------- - None - """ - - if not file_dict: - return - - try: - # Create group and add their attributes - filename = file_dict['name'] - group = h5file[group_name].create_group(name=filename) - # Add group attributes - group.attrs.update(file_dict['attributes_dict']) - - # Add datasets to the just created group - for dataset in file_dict['datasets']: - dataset_obj = group.create_dataset( - name=dataset['name'], - data=dataset['data'], - shape=dataset['shape'] - ) - - # Add dataset's attributes - attributes = dataset.get('attributes', {}) - dataset_obj.attrs.update(attributes) - group.attrs['last_update_date'] = utils.created_at().encode('utf-8') - - stdout = f'Completed transfer for /{group_name}/{filename}' - - except Exception as inst: - stdout = inst - logging.error('Failed to transfer data into HDF5: %s', inst) - - return stdout - -def __copy_file_in_group(source_file_path, dest_file_obj : h5py.File, dest_group_name, work_with_copy : bool = True): - # Create copy of original file to avoid possible file corruption and work with it. - - if work_with_copy: - tmp_file_path = utils.make_file_copy(source_file_path) - else: - tmp_file_path = source_file_path - - # Open backup h5 file and copy complet filesystem directory onto a group in h5file - with h5py.File(tmp_file_path,'r') as src_file: - dest_file_obj.copy(source= src_file['/'], dest= dest_group_name) - - if 'tmp_files' in tmp_file_path: - os.remove(tmp_file_path) - - stdout = f'Completed transfer for /{dest_group_name}' - return stdout - -def create_hdf5_file_from_filesystem_path(path_to_input_directory: str, - path_to_filenames_dict: dict = None, - select_dir_keywords : list = [], - root_metadata_dict : dict = {}, mode = 'w'): - - """ - Creates an .h5 file with name "output_filename" that preserves the directory tree (or folder structure) - of a given filesystem path. - - The data integration capabilities are limited by our file reader, which can only access data from a list of - admissible file formats. These, however, can be extended. Directories are groups in the resulting HDF5 file. - Files are formatted as composite objects consisting of a group, file, and attributes. - - Parameters - ---------- - output_filename : str - Name of the output HDF5 file. - path_to_input_directory : str - Path to root directory, specified with forward slashes, e.g., path/to/root. - - path_to_filenames_dict : dict, optional - A pre-processed dictionary where keys are directory paths on the input directory's tree and values are lists of files. - If provided, 'input_file_system_path' is ignored. - - select_dir_keywords : list - List of string elements to consider or select only directory paths that contain - a word in 'select_dir_keywords'. When empty, all directory paths are considered - to be included in the HDF5 file group hierarchy. - root_metadata_dict : dict - Metadata to include at the root level of the HDF5 file. - - mode : str - 'w' create File, truncate if it exists, or 'r+' read/write, File must exists. By default, mode = "w". - - Returns - ------- - output_filename : str - Path to the created HDF5 file. - """ - - - if not mode in ['w','r+']: - raise ValueError(f'Parameter mode must take values in ["w","r+"]') - - if not '/' in path_to_input_directory: - raise ValueError('path_to_input_directory needs to be specified using forward slashes "/".' ) - - #path_to_output_directory = os.path.join(path_to_input_directory,'..') - path_to_input_directory = os.path.normpath(path_to_input_directory).rstrip(os.sep) - - - for i, keyword in enumerate(select_dir_keywords): - select_dir_keywords[i] = keyword.replace('/',os.sep) - - if not path_to_filenames_dict: - # On dry_run=True, returns path to files dictionary of the output directory without making a actual copy of the input directory. - # Therefore, there wont be a copying conflict by setting up input and output directories the same - path_to_filenames_dict = utils.copy_directory_with_contraints(input_dir_path=path_to_input_directory, - output_dir_path=path_to_input_directory, - dry_run=True) - # Set input_directory as copied input directory - root_dir = path_to_input_directory - path_to_output_file = path_to_input_directory.rstrip(os.path.sep) + '.h5' - - start_message = f'\n[Start] Data integration :\nSource: {path_to_input_directory}\nDestination: {path_to_output_file}\n' - - print(start_message) - logging.info(start_message) - - # Check if the .h5 file already exists - if os.path.exists(path_to_output_file) and mode in ['w']: - message = ( - f"[Notice] The file '{path_to_output_file}' already exists and will not be overwritten.\n" - "If you wish to replace it, please delete the existing file first and rerun the program." - ) - print(message) - logging.error(message) - else: - with h5py.File(path_to_output_file, mode=mode, track_order=True) as h5file: - - number_of_dirs = len(path_to_filenames_dict.keys()) - dir_number = 1 - for dirpath, filtered_filenames_list in path_to_filenames_dict.items(): - - # Check if filtered_filenames_list is nonempty. TODO: This is perhaps redundant by design of path_to_filenames_dict. - if not filtered_filenames_list: - continue - - group_name = dirpath.replace(os.sep,'/') - group_name = group_name.replace(root_dir.replace(os.sep,'/') + '/', '/') - - # Flatten group name to one level - if select_dir_keywords: - offset = sum([len(i.split(os.sep)) if i in dirpath else 0 for i in select_dir_keywords]) - else: - offset = 1 - tmp_list = group_name.split('/') - if len(tmp_list) > offset+1: - group_name = '/'.join([tmp_list[i] for i in range(offset+1)]) - - # Create group called "group_name". Hierarchy of nested groups can be implicitly defined by the forward slashes - if not group_name in h5file.keys(): - h5file.create_group(group_name) - h5file[group_name].attrs['creation_date'] = utils.created_at().encode('utf-8') - #h5file[group_name].attrs.create(name='filtered_file_list',data=convert_string_to_bytes(filtered_filename_list)) - #h5file[group_name].attrs.create(name='file_list',data=convert_string_to_bytes(filenames_list)) - #else: - #print(group_name,' was already created.') - instFoldermsgStart = f'Starting data transfer from instFolder: {group_name}' - print(instFoldermsgStart) - - for filenumber, filename in enumerate(filtered_filenames_list): - - #file_ext = os.path.splitext(filename)[1] - #try: - - # hdf5 path to filename group - dest_group_name = f'{group_name}/{filename}' - - if not 'h5' in filename: - #file_dict = config_file.select_file_readers(group_id)[file_ext](os.path.join(dirpath,filename)) - #file_dict = ext_to_reader_dict[file_ext](os.path.join(dirpath,filename)) - file_dict = filereader_registry.select_file_reader(dest_group_name)(os.path.join(dirpath,filename)) - - stdout = __transfer_file_dict_to_hdf5(h5file, group_name, file_dict) - - else: - source_file_path = os.path.join(dirpath,filename) - dest_file_obj = h5file - #group_name +'/'+filename - #ext_to_reader_dict[file_ext](source_file_path, dest_file_obj, dest_group_name) - #g5505f_reader.select_file_reader(dest_group_name)(source_file_path, dest_file_obj, dest_group_name) - stdout = __copy_file_in_group(source_file_path, dest_file_obj, dest_group_name, False) - - # Update the progress bar and log the end message - instFoldermsdEnd = f'\nCompleted data transfer for instFolder: {group_name}\n' - # Print and log the start message - utils.progressBar(dir_number, number_of_dirs, instFoldermsdEnd) - logging.info(instFoldermsdEnd ) - dir_number = dir_number + 1 - - print('[End] Data integration') - logging.info('[End] Data integration') - - if len(root_metadata_dict.keys())>0: - for key, value in root_metadata_dict.items(): - #if key in h5file.attrs: - # del h5file.attrs[key] - h5file.attrs.create(key, value) - #annotate_root_dir(output_filename,root_metadata_dict) - - - #output_yml_filename_path = hdf5_vis.take_yml_snapshot_of_hdf5_file(output_filename) - - return path_to_output_file #, output_yml_filename_path - -def create_hdf5_file_from_dataframe(ofilename, input_data, group_by_funcs: list, approach: str = None, extract_attrs_func=None): - """ - Creates an HDF5 file with hierarchical groups based on the specified grouping functions or columns. - - Parameters: - ----------- - ofilename (str): Path for the output HDF5 file. - input_data (pd.DataFrame or str): Input data as a DataFrame or a valid file system path. - group_by_funcs (list): List of callables or column names to define hierarchical grouping. - approach (str): Specifies the approach ('top-down' or 'bottom-up') for creating the HDF5 file. - extract_attrs_func (callable, optional): Function to extract additional attributes for HDF5 groups. - - Returns: - -------- - None - """ - # Check whether input_data is a valid file-system path or a DataFrame - is_valid_path = lambda x: os.path.exists(x) if isinstance(x, str) else False - - if is_valid_path(input_data): - # If input_data is a file-system path, create a DataFrame with file info - file_list = os.listdir(input_data) - df = pd.DataFrame(file_list, columns=['filename']) - df = utils.augment_with_filetype(df) # Add filetype information if needed - elif isinstance(input_data, pd.DataFrame): - # If input_data is a DataFrame, make a copy - df = input_data.copy() - else: - raise ValueError("input_data must be either a valid file-system path or a DataFrame.") - - # Generate grouping columns based on group_by_funcs - if utils.is_callable_list(group_by_funcs): - grouping_cols = [] - for i, func in enumerate(group_by_funcs): - col_name = f'level_{i}_groups' - grouping_cols.append(col_name) - df[col_name] = func(df) - elif utils.is_str_list(group_by_funcs) and all([item in df.columns for item in group_by_funcs]): - grouping_cols = group_by_funcs - else: - raise ValueError("'group_by_funcs' must be a list of callables or valid column names in the DataFrame.") - - # Generate group paths - df['group_path'] = ['/' + '/'.join(row) for row in df[grouping_cols].values.astype(str)] - - # Open the HDF5 file in write mode - with h5py.File(ofilename, 'w') as file: - for group_path in df['group_path'].unique(): - # Create groups in HDF5 - group = file.create_group(group_path) - - # Filter the DataFrame for the current group - datatable = df[df['group_path'] == group_path].copy() - - # Drop grouping columns and the generated 'group_path' - datatable = datatable.drop(columns=grouping_cols + ['group_path']) - - # Add datasets to groups if data exists - if not datatable.empty: - dataset = utils.convert_dataframe_to_np_structured_array(datatable) - group.create_dataset(name='data_table', data=dataset) - - # Add attributes if extract_attrs_func is provided - if extract_attrs_func: - attrs = extract_attrs_func(datatable) - for key, value in attrs.items(): - group.attrs[key] = value - - # Save metadata about depth of hierarchy - file.attrs.create(name='depth', data=len(grouping_cols) - 1) - - print(f"HDF5 file created successfully at {ofilename}") - - return ofilename - - -def save_processed_dataframe_to_hdf5(df, annotator, output_filename): # src_hdf5_path, script_date, script_name): - """ - Save processed dataframe columns with annotations to an HDF5 file. - - Parameters: - df (pd.DataFrame): DataFrame containing processed time series. - annotator (): Annotator object with get_metadata method. - output_filename (str): Path to the source HDF5 file. - """ - # Convert datetime columns to string - datetime_cols = df.select_dtypes(include=['datetime64']).columns - - if list(datetime_cols): - df[datetime_cols] = df[datetime_cols].map(str) - - # Convert dataframe to structured array - icad_data_table = utils.convert_dataframe_to_np_structured_array(df) - - # Get metadata - metadata_dict = annotator.get_metadata() - - # Prepare project level attributes to be added at the root level - - project_level_attributes = metadata_dict['metadata']['project'] - - # Prepare high-level attributes - high_level_attributes = { - 'parent_files': metadata_dict['parent_files'], - **metadata_dict['metadata']['sample'], - **metadata_dict['metadata']['environment'], - **metadata_dict['metadata']['instruments'] - } - - # Prepare data level attributes - data_level_attributes = metadata_dict['metadata']['datasets'] - - for key, value in data_level_attributes.items(): - if isinstance(value,dict): - data_level_attributes[key] = utils.convert_attrdict_to_np_structured_array(value) - - - # Prepare file dictionary - file_dict = { - 'name': project_level_attributes['processing_file'], - 'attributes_dict': high_level_attributes, - 'datasets': [{ - 'name': "data_table", - 'data': icad_data_table, - 'shape': icad_data_table.shape, - 'attributes': data_level_attributes - }] - } - - # Check if the file exists - if os.path.exists(output_filename): - mode = "a" - print(f"File {output_filename} exists. Opening in append mode.") - else: - mode = "w" - print(f"File {output_filename} does not exist. Creating a new file.") - - - # Write to HDF5 - with h5py.File(output_filename, mode) as h5file: - # Add project level attributes at the root/top level - h5file.attrs.update(project_level_attributes) - __transfer_file_dict_to_hdf5(h5file, '/', file_dict) - -#if __name__ == '__main__': +import sys +import os +root_dir = os.path.abspath(os.curdir) +sys.path.append(root_dir) + +import pandas as pd +import numpy as np +import h5py +import logging + +import utils.g5505_utils as utils +import instruments.readers.filereader_registry as filereader_registry + + + +def __transfer_file_dict_to_hdf5(h5file, group_name, file_dict): + """ + Transfers data from a file_dict to an HDF5 file. + + Parameters + ---------- + h5file : h5py.File + HDF5 file object where the data will be written. + group_name : str + Name of the HDF5 group where data will be stored. + file_dict : dict + Dictionary containing file data to be transferred. Required structure: + { + 'name': str, + 'attributes_dict': dict, + 'datasets': [ + { + 'name': str, + 'data': array-like, + 'shape': tuple, + 'attributes': dict (optional) + }, + ... + ] + } + + Returns + ------- + None + """ + + if not file_dict: + return + + try: + # Create group and add their attributes + filename = file_dict['name'] + group = h5file[group_name].create_group(name=filename) + # Add group attributes + group.attrs.update(file_dict['attributes_dict']) + + # Add datasets to the just created group + for dataset in file_dict['datasets']: + dataset_obj = group.create_dataset( + name=dataset['name'], + data=dataset['data'], + shape=dataset['shape'] + ) + + # Add dataset's attributes + attributes = dataset.get('attributes', {}) + dataset_obj.attrs.update(attributes) + group.attrs['last_update_date'] = utils.created_at().encode('utf-8') + + stdout = f'Completed transfer for /{group_name}/{filename}' + + except Exception as inst: + stdout = inst + logging.error('Failed to transfer data into HDF5: %s', inst) + + return stdout + +def __copy_file_in_group(source_file_path, dest_file_obj : h5py.File, dest_group_name, work_with_copy : bool = True): + # Create copy of original file to avoid possible file corruption and work with it. + + if work_with_copy: + tmp_file_path = utils.make_file_copy(source_file_path) + else: + tmp_file_path = source_file_path + + # Open backup h5 file and copy complet filesystem directory onto a group in h5file + with h5py.File(tmp_file_path,'r') as src_file: + dest_file_obj.copy(source= src_file['/'], dest= dest_group_name) + + if 'tmp_files' in tmp_file_path: + os.remove(tmp_file_path) + + stdout = f'Completed transfer for /{dest_group_name}' + return stdout + +def create_hdf5_file_from_filesystem_path(path_to_input_directory: str, + path_to_filenames_dict: dict = None, + select_dir_keywords : list = [], + root_metadata_dict : dict = {}, mode = 'w'): + + """ + Creates an .h5 file with name "output_filename" that preserves the directory tree (or folder structure) + of a given filesystem path. + + The data integration capabilities are limited by our file reader, which can only access data from a list of + admissible file formats. These, however, can be extended. Directories are groups in the resulting HDF5 file. + Files are formatted as composite objects consisting of a group, file, and attributes. + + Parameters + ---------- + output_filename : str + Name of the output HDF5 file. + path_to_input_directory : str + Path to root directory, specified with forward slashes, e.g., path/to/root. + + path_to_filenames_dict : dict, optional + A pre-processed dictionary where keys are directory paths on the input directory's tree and values are lists of files. + If provided, 'input_file_system_path' is ignored. + + select_dir_keywords : list + List of string elements to consider or select only directory paths that contain + a word in 'select_dir_keywords'. When empty, all directory paths are considered + to be included in the HDF5 file group hierarchy. + root_metadata_dict : dict + Metadata to include at the root level of the HDF5 file. + + mode : str + 'w' create File, truncate if it exists, or 'r+' read/write, File must exists. By default, mode = "w". + + Returns + ------- + output_filename : str + Path to the created HDF5 file. + """ + + + if not mode in ['w','r+']: + raise ValueError(f'Parameter mode must take values in ["w","r+"]') + + if not '/' in path_to_input_directory: + raise ValueError('path_to_input_directory needs to be specified using forward slashes "/".' ) + + #path_to_output_directory = os.path.join(path_to_input_directory,'..') + path_to_input_directory = os.path.normpath(path_to_input_directory).rstrip(os.sep) + + + for i, keyword in enumerate(select_dir_keywords): + select_dir_keywords[i] = keyword.replace('/',os.sep) + + if not path_to_filenames_dict: + # On dry_run=True, returns path to files dictionary of the output directory without making a actual copy of the input directory. + # Therefore, there wont be a copying conflict by setting up input and output directories the same + path_to_filenames_dict = utils.copy_directory_with_contraints(input_dir_path=path_to_input_directory, + output_dir_path=path_to_input_directory, + dry_run=True) + # Set input_directory as copied input directory + root_dir = path_to_input_directory + path_to_output_file = path_to_input_directory.rstrip(os.path.sep) + '.h5' + + start_message = f'\n[Start] Data integration :\nSource: {path_to_input_directory}\nDestination: {path_to_output_file}\n' + + print(start_message) + logging.info(start_message) + + # Check if the .h5 file already exists + if os.path.exists(path_to_output_file) and mode in ['w']: + message = ( + f"[Notice] The file '{path_to_output_file}' already exists and will not be overwritten.\n" + "If you wish to replace it, please delete the existing file first and rerun the program." + ) + print(message) + logging.error(message) + else: + with h5py.File(path_to_output_file, mode=mode, track_order=True) as h5file: + + number_of_dirs = len(path_to_filenames_dict.keys()) + dir_number = 1 + for dirpath, filtered_filenames_list in path_to_filenames_dict.items(): + + # Check if filtered_filenames_list is nonempty. TODO: This is perhaps redundant by design of path_to_filenames_dict. + if not filtered_filenames_list: + continue + + group_name = dirpath.replace(os.sep,'/') + group_name = group_name.replace(root_dir.replace(os.sep,'/') + '/', '/') + + # Flatten group name to one level + if select_dir_keywords: + offset = sum([len(i.split(os.sep)) if i in dirpath else 0 for i in select_dir_keywords]) + else: + offset = 1 + tmp_list = group_name.split('/') + if len(tmp_list) > offset+1: + group_name = '/'.join([tmp_list[i] for i in range(offset+1)]) + + # Create group called "group_name". Hierarchy of nested groups can be implicitly defined by the forward slashes + if not group_name in h5file.keys(): + h5file.create_group(group_name) + h5file[group_name].attrs['creation_date'] = utils.created_at().encode('utf-8') + #h5file[group_name].attrs.create(name='filtered_file_list',data=convert_string_to_bytes(filtered_filename_list)) + #h5file[group_name].attrs.create(name='file_list',data=convert_string_to_bytes(filenames_list)) + #else: + #print(group_name,' was already created.') + instFoldermsgStart = f'Starting data transfer from instFolder: {group_name}' + print(instFoldermsgStart) + + for filenumber, filename in enumerate(filtered_filenames_list): + + #file_ext = os.path.splitext(filename)[1] + #try: + + # hdf5 path to filename group + dest_group_name = f'{group_name}/{filename}' + + if not 'h5' in filename: + #file_dict = config_file.select_file_readers(group_id)[file_ext](os.path.join(dirpath,filename)) + #file_dict = ext_to_reader_dict[file_ext](os.path.join(dirpath,filename)) + file_dict = filereader_registry.select_file_reader(dest_group_name)(os.path.join(dirpath,filename)) + + stdout = __transfer_file_dict_to_hdf5(h5file, group_name, file_dict) + + else: + source_file_path = os.path.join(dirpath,filename) + dest_file_obj = h5file + #group_name +'/'+filename + #ext_to_reader_dict[file_ext](source_file_path, dest_file_obj, dest_group_name) + #g5505f_reader.select_file_reader(dest_group_name)(source_file_path, dest_file_obj, dest_group_name) + stdout = __copy_file_in_group(source_file_path, dest_file_obj, dest_group_name, False) + + # Update the progress bar and log the end message + instFoldermsdEnd = f'\nCompleted data transfer for instFolder: {group_name}\n' + # Print and log the start message + utils.progressBar(dir_number, number_of_dirs, instFoldermsdEnd) + logging.info(instFoldermsdEnd ) + dir_number = dir_number + 1 + + print('[End] Data integration') + logging.info('[End] Data integration') + + if len(root_metadata_dict.keys())>0: + for key, value in root_metadata_dict.items(): + #if key in h5file.attrs: + # del h5file.attrs[key] + h5file.attrs.create(key, value) + #annotate_root_dir(output_filename,root_metadata_dict) + + + #output_yml_filename_path = hdf5_vis.take_yml_snapshot_of_hdf5_file(output_filename) + + return path_to_output_file #, output_yml_filename_path + +def create_hdf5_file_from_dataframe(ofilename, input_data, group_by_funcs: list, approach: str = None, extract_attrs_func=None): + """ + Creates an HDF5 file with hierarchical groups based on the specified grouping functions or columns. + + Parameters: + ----------- + ofilename (str): Path for the output HDF5 file. + input_data (pd.DataFrame or str): Input data as a DataFrame or a valid file system path. + group_by_funcs (list): List of callables or column names to define hierarchical grouping. + approach (str): Specifies the approach ('top-down' or 'bottom-up') for creating the HDF5 file. + extract_attrs_func (callable, optional): Function to extract additional attributes for HDF5 groups. + + Returns: + -------- + None + """ + # Check whether input_data is a valid file-system path or a DataFrame + is_valid_path = lambda x: os.path.exists(x) if isinstance(x, str) else False + + if is_valid_path(input_data): + # If input_data is a file-system path, create a DataFrame with file info + file_list = os.listdir(input_data) + df = pd.DataFrame(file_list, columns=['filename']) + df = utils.augment_with_filetype(df) # Add filetype information if needed + elif isinstance(input_data, pd.DataFrame): + # If input_data is a DataFrame, make a copy + df = input_data.copy() + else: + raise ValueError("input_data must be either a valid file-system path or a DataFrame.") + + # Generate grouping columns based on group_by_funcs + if utils.is_callable_list(group_by_funcs): + grouping_cols = [] + for i, func in enumerate(group_by_funcs): + col_name = f'level_{i}_groups' + grouping_cols.append(col_name) + df[col_name] = func(df) + elif utils.is_str_list(group_by_funcs) and all([item in df.columns for item in group_by_funcs]): + grouping_cols = group_by_funcs + else: + raise ValueError("'group_by_funcs' must be a list of callables or valid column names in the DataFrame.") + + # Generate group paths + df['group_path'] = ['/' + '/'.join(row) for row in df[grouping_cols].values.astype(str)] + + # Open the HDF5 file in write mode + with h5py.File(ofilename, 'w') as file: + for group_path in df['group_path'].unique(): + # Create groups in HDF5 + group = file.create_group(group_path) + + # Filter the DataFrame for the current group + datatable = df[df['group_path'] == group_path].copy() + + # Drop grouping columns and the generated 'group_path' + datatable = datatable.drop(columns=grouping_cols + ['group_path']) + + # Add datasets to groups if data exists + if not datatable.empty: + dataset = utils.convert_dataframe_to_np_structured_array(datatable) + group.create_dataset(name='data_table', data=dataset) + + # Add attributes if extract_attrs_func is provided + if extract_attrs_func: + attrs = extract_attrs_func(datatable) + for key, value in attrs.items(): + group.attrs[key] = value + + # Save metadata about depth of hierarchy + file.attrs.create(name='depth', data=len(grouping_cols) - 1) + + print(f"HDF5 file created successfully at {ofilename}") + + return ofilename + + +def save_processed_dataframe_to_hdf5(df, annotator, output_filename): # src_hdf5_path, script_date, script_name): + """ + Save processed dataframe columns with annotations to an HDF5 file. + + Parameters: + df (pd.DataFrame): DataFrame containing processed time series. + annotator (): Annotator object with get_metadata method. + output_filename (str): Path to the source HDF5 file. + """ + # Convert datetime columns to string + datetime_cols = df.select_dtypes(include=['datetime64']).columns + + if list(datetime_cols): + df[datetime_cols] = df[datetime_cols].map(str) + + # Convert dataframe to structured array + icad_data_table = utils.convert_dataframe_to_np_structured_array(df) + + # Get metadata + metadata_dict = annotator.get_metadata() + + # Prepare project level attributes to be added at the root level + + project_level_attributes = metadata_dict['metadata']['project'] + + # Prepare high-level attributes + high_level_attributes = { + 'parent_files': metadata_dict['parent_files'], + **metadata_dict['metadata']['sample'], + **metadata_dict['metadata']['environment'], + **metadata_dict['metadata']['instruments'] + } + + # Prepare data level attributes + data_level_attributes = metadata_dict['metadata']['datasets'] + + for key, value in data_level_attributes.items(): + if isinstance(value,dict): + data_level_attributes[key] = utils.convert_attrdict_to_np_structured_array(value) + + + # Prepare file dictionary + file_dict = { + 'name': project_level_attributes['processing_file'], + 'attributes_dict': high_level_attributes, + 'datasets': [{ + 'name': "data_table", + 'data': icad_data_table, + 'shape': icad_data_table.shape, + 'attributes': data_level_attributes + }] + } + + # Check if the file exists + if os.path.exists(output_filename): + mode = "a" + print(f"File {output_filename} exists. Opening in append mode.") + else: + mode = "w" + print(f"File {output_filename} does not exist. Creating a new file.") + + + # Write to HDF5 + with h5py.File(output_filename, mode) as h5file: + # Add project level attributes at the root/top level + h5file.attrs.update(project_level_attributes) + __transfer_file_dict_to_hdf5(h5file, '/', file_dict) + +#if __name__ == '__main__': diff --git a/src/openbis_lib.py b/src/openbis_lib.py index d9a34e9..184f6fc 100644 --- a/src/openbis_lib.py +++ b/src/openbis_lib.py @@ -1,270 +1,270 @@ -import pandas as pd -import logging -import os -import datetime -from pybis import Openbis -import hidden - -admissible_props_list = ['$name', 'filenumber', 'default_experiment.experimental_results', - 'dataquality', '$xmlcomments', '$annotations_state', - 'sample_name', 'position_x', 'position_y', 'position_z', 'temp', 'cell_pressure', 'gas_flow_setting', 'sample_notes', - 'beamline', 'photon_energy', 'slit_entrance_v', 'slit_exit_v', 'izero', - 'slit_exit_h', 'hos', 'cone', 'endstation', 'hof', - 'method_name', 'region', 'lens_mode', 'acq_mode', 'dwell_time', 'frames', 'passenergy', - 'iterations', 'sequenceiterations', 'ke_range_center', 'ke_step'] - - -def initialize_openbis_obj(): - - # TODO: implement a more secure authentication method. - openbis_obj = Openbis('https://openbis-psi.labnotebook.ch/openbis/webapp/eln-lims/?menuUniqueId=null&viewName=showBlancPage&viewData=null', verify_certificates=False) - openbis_obj.login(hidden.username,hidden.password) - - return openbis_obj - -def align_datetime_observation_windows(df_h5: pd.DataFrame, df_openbis: pd.DataFrame, h5_datetime_var: str = 'lastModifiedDatestr', ob_datetime_var: str = 'registrationDate') -> pd.DataFrame: - - """ returns filtered/reduced versions of 'df' and 'df_ref' with aligned datetime observation windows. - That is, the datetime variable range is the same for the returned dataframes.""" - #""returns a filtered or reduced version of 'df' by removing all rows that are outside the datetime variable overlapping region between 'df' and 'df_ref'. - #""" - - #df_h5['lastModifiedDatestr'] = df_h5['lastModifiedDatestr'].astype('datetime64[ns]') - #df_h5 = df_h5.sort_values(by='lastModifiedDatestr') - - if not (h5_datetime_var in df_h5.columns.to_list() and ob_datetime_var in df_openbis.columns.to_list()): - #TODO: Check if ValueError is the best type of error to raise here - raise ValueError("Dataframes 'df' and 'df_ref' must contain columns 'datetime_var' and 'datetime_var_ref', storing values in suitable datetime string format (e.g., yyyy-mm-dd hh:mm:ss).") - - df_h5[h5_datetime_var] = df_h5[h5_datetime_var].astype('datetime64[ns]') - df_openbis[ob_datetime_var] = df_openbis[ob_datetime_var].astype('datetime64[ns]') - - min_timestamp = max([df_openbis[ob_datetime_var].min(), df_h5[h5_datetime_var].min()]) - max_timestamp = min([df_openbis[ob_datetime_var].max(), df_h5[h5_datetime_var].max()]) - - # Determine overlap between df and df_ref, and filters out all rows from df with datetime variable outside the overlapping datetime region. - datetime_overlap_indicator = (df_h5[h5_datetime_var] >= min_timestamp) & (df_h5[h5_datetime_var] <= max_timestamp) - df_h5 = df_h5.loc[datetime_overlap_indicator,:] - - datetime_overlap_indicator = (df_openbis[ob_datetime_var] >= min_timestamp) & (df_openbis[ob_datetime_var] <= max_timestamp) - df_openbis = df_openbis.loc[datetime_overlap_indicator,:] - - df_h5 = df_h5.sort_values(by=h5_datetime_var) - df_openbis = df_openbis.sort_values(by=ob_datetime_var) - - return df_h5, df_openbis - -def reformat_openbis_dataframe_filenumber(df_openbis): - - if not 'FILENUMBER' in df_openbis.columns: - raise ValueError('df_openbis does not contain the column "FILENUMBER". Make sure you query (e.g., o.get_samples(props=["filenumbe"])) that before creating df_openbis.') - #if not 'name' in df.columns: - # raise ValueError("df does not contain the column 'name'. Ensure df complies with Throsten's Table's format.") - - # Augment df_openbis with 'name' column consitent with Thorsten's naming convention - name_list = ['0' + item.zfill(3) + item.zfill(3) for item in df_openbis['FILENUMBER']] - df_openbis['REFORMATED_FILENUMBER'] = pd.Series(name_list, index=df_openbis.index) - - return df_openbis - -def pair_openbis_and_h5_dataframes(df_openbis, df_h5, pairing_ob_var: str, pairing_h5_var: str): - - """ Pairs every row (or openbis sample) in 'df_openbis' with a set of rows (or measurements) in 'df_h5' by matching the i-th row in 'df_h5' - with the rows of 'df_h5' that satisfy the string df_openbis.loc[i,pairing_var_1] is contained in the string df_h5[i,pairing_var_2] - - Example: pairing_var_1, pairing_var_2 = reformated 'REFORMATED_FILENUMBER', 'name' - - """ - # Reformat openbis dataframe filenumber so that it can be used to find associated measurements in h5 dataframe - df_openbis = reformat_openbis_dataframe_filenumber(df_openbis) - - related_indices_list = [] - for sample_idx in df_openbis.index: - sample_value = df_openbis.loc[sample_idx,pairing_ob_var] - tmp_list = [sample_value in item[0:item.find('_')] for item in df_h5[pairing_h5_var]] - related_indices_list.append(df_h5.index[tmp_list]) - - print('Paring openbis sample: ' + df_openbis.loc[sample_idx,pairing_ob_var]) - print('with reformated FILENUMBER: ' + sample_value) - print('to following measurements in h5 dataframe:') - print(df_h5.loc[df_h5.index[tmp_list],'name']) - print('\n') - - df_openbis['related_h5_indices'] = pd.Series(related_indices_list, index=df_openbis.index) - - return df_openbis - - -def range_cols_2_string(df,lb_var,ub_var): - - if not sum(df.loc[:,ub_var]-df.loc[:,lb_var])==0: - #tmp_list = ['-'.join([str(round(df.loc[i,lb_var],2)),str(round(df.loc[i,ub_var],1))]) for i in df.index] - tmp_list = ['-'.join(["{:.1f}".format(df.loc[i,lb_var]),"{:.1f}".format(df.loc[i,ub_var])]) for i in df.index] - elif len(df.loc[:,lb_var].unique())>1: # check if values are different - #tmp_list = [str(round(df.loc[i,lb_var],2)) for i in df.index] - tmp_list = ["{:.1f}".format(df.loc[i,lb_var]) for i in df.index] - else: - #tmp_list = [str(round(df.loc[0,lb_var],2))] - tmp_list = ["{:.1f}".format(df.loc[0,lb_var])] - return '/'.join(tmp_list) - -def col_2_string(df,column_var): - - if not column_var in df.columns: - raise ValueError("'column var must belong in df.columns") - - #tmp_list = [str(round(item,1)) for item in df[column_var]] - tmp_list = ["{:.2f}".format(item) for item in df[column_var]] - if len(df[column_var].unique())==1: - tmp_list = [tmp_list[0]] - - return '/'.join(tmp_list) - - -def compute_openbis_sample_props_from_h5(df_openbis, df_h5, sample_idx): - - prop2attr = {'sample_name':'sample', # ask Throsten whether this assignment is correct or not - 'position_x':'smplX_mm', - 'position_y':'smplY_mm', - 'position_z':'smplZ_mm', - 'temp':'sampleTemp_dC', - 'cell_pressure':'cellPressure_mbar', - #'gas_flow_setting': '', - 'method_name':'regionName', # measurement type: XPS or NEXAFS - 'region':'regionName', # VB/N1s/C1s - 'passenergy':'regionName', # REAL - - 'photon_energy':'xRayEkinRange_eV', - 'dwell_time':'scientaDwellTime_ms', - 'acq_mode':'scientaAcquisitionMode', - 'ke_range_center':'scientaEkinRange_eV', - 'ke_step':'scientaEkinStep_eV', - 'lens_mode':'scientaLensMode' - } - - sample_identifier = df_openbis.loc[sample_idx,'identifier'] - props_dict = {'FILENUMBER' : df_openbis.loc[sample_idx,'FILENUMBER']} - - #props_dict = {} - - if not len(df_openbis.loc[sample_idx,'related_h5_indices']): - props_dict['identifier'] = sample_identifier - return props_dict - - reduced_df_h5 = df_h5.loc[df_openbis.loc[sample_idx,'related_h5_indices'],:] - reduced_df_h5 = reduced_df_h5.reset_index() - - # include related_samples key for validation purposes. Related samples are used to compute average and/or combined openbis properties. - related_sample_list = [reduced_df_h5['name'][index] for index in reduced_df_h5['name'].index] - related_samples = ' / '.join(related_sample_list) - props_dict['Subject_samples'] = related_samples - - props_dict['sample_name'] = reduced_df_h5['sample'].unique()[0] if len(reduced_df_h5['sample'].unique())==1 else '/'.join(reduced_df_h5['sample'].tolist()) - - if not 'NEXAFS' in reduced_df_h5['regionName'].iloc[0]: - props_dict['identifier'] = sample_identifier - props_dict['method_name'] = 'XPS' - for item_idx in reduced_df_h5.index: - item = reduced_df_h5.loc[item_idx,'regionName'] - if item_idx > 0: - props_dict['region'] = props_dict['region'] + '/' + item[0:item.find('_')] - #props_dict['dwell_time'] = props_dict['dwell_time'] + '/' + str(reduced_df_h5.loc[item_idx,'scientaDwellTime_ms']) - #props_dict['ke_range_center'] = props_dict['ke_range_center'] + '/' + str(round(reduced_df_h5.loc[item_idx,['scientaEkinRange_eV_1','scientaEkinRange_eV_2']].mean(),2)) - #props_dict['ke_step_center'] = props_dict['ke_step_center'] + '/' + str(reduced_df_h5.loc[item_idx,'scientaEkinStep_eV']) - #props_dict['passenergy'].append(float(item[item.find('_')+1:item.find('eV')])) - else: - props_dict['region'] = item[0:item.find('_')] - #props_dict['dwell_time'] = str(reduced_df_h5.loc[item_idx,'scientaDwellTime_ms']) - #props_dict['ke_range_center'] = str(round(reduced_df_h5.loc[item_idx,['scientaEkinRange_eV_1','scientaEkinRange_eV_2']].mean(),2)) - #props_dict['ke_step_center'] = str(reduced_df_h5.loc[item_idx,'scientaEkinStep_eV']) - - #props_dict['passenergy'] = reduced_df_h5.loc[:,'scientaPassEnergy_eV'].min() - - else: - props_dict = {'identifier':sample_identifier,'method_name':'NEXAFS'} - - - #props_dict['temp'] = round(reduced_df_h5['sampleTemp_dC'].mean(),2) - #props_dict['cell_pressure'] = round(reduced_df_h5['cellPressure_mbar'].mean(),2) - props_dict['temp'] = "{:.2f}".format(reduced_df_h5['sampleTemp_dC'].mean()) - props_dict['cell_pressure'] = "{:.2f}".format(reduced_df_h5['cellPressure_mbar'].mean()) - - reduced_df_h5['scientaDwellTime_ms'] = reduced_df_h5['scientaDwellTime_ms']*1e-3 # covert ms to seconds - props_dict['dwell_time'] = col_2_string(reduced_df_h5,'scientaDwellTime_ms') - props_dict['passenergy'] = col_2_string(reduced_df_h5,'scientaPassEnergy_eV') - props_dict['ke_step_center'] = col_2_string(reduced_df_h5,'scientaEkinStep_eV') - #props_dict['photon_energy'] =round(reduced_df_h5[['xRayEkinRange_eV_1','xRayEkinRange_eV_2']].mean(axis=1)[0],2) - props_dict['photon_energy'] = range_cols_2_string(reduced_df_h5,'xRayEkinRange_eV_1','xRayEkinRange_eV_2') - props_dict['ke_range_center'] = range_cols_2_string(reduced_df_h5,'scientaEkinRange_eV_1','scientaEkinRange_eV_2') - - props_dict['lens_mode'] = reduced_df_h5['scientaLensMode'][0] - props_dict['acq_mode'] = reduced_df_h5['scientaAcquisitionMode'][0] - - props_dict['position_x'] = "{:.2f}".format(reduced_df_h5.loc[:,'smplX_mm'].mean()) # round(reduced_df_h5.loc[:,'smplX_mm'].mean(),2) - props_dict['position_y'] = "{:.2f}".format(reduced_df_h5.loc[:,'smplY_mm'].mean()) - props_dict['position_z'] = "{:.2f}".format(reduced_df_h5.loc[:,'smplZ_mm'].mean()) - - - - return props_dict - - - -def single_sample_update(sample_props_dict,sample_collection,props_include_list): - - """ Updates sample in openbis database specified in sample_props_dict, which must belong in sample_collection (i.e., result of openbis_obj.get_samples(...)) """ - - try: - sample_path_identifier = sample_props_dict['identifier'] #path-like index - sample = sample_collection[sample_path_identifier] - for prop in sample_props_dict.keys(): - if (prop in admissible_props_list) and (prop in props_include_list): - sample.props[prop] = sample_props_dict[prop] - sample.save() - except Exception: - logging.error(Exception) - - return 0 - - -def sample_batch_update(openbis_obj,sample_collection,df_openbis,df_h5,props_include_list): - - """ See """ - - if not 'related_h5_indices' in df_openbis.columns: - raise ValueError("Input dataframe 'df_openbis' must contain a column named 'related_h5_indeces', resulting from suitable proprocessing steps.") - - # TODO: as a safeguard, create exclude list containing properties that must not be changed - exclude_list = ['filenumber','FILENUMBER','identifier'] - for item in props_include_list: - if item in exclude_list: - props_include_list.remove(item) - - trans = openbis_obj.new_transaction() - for sample_idx in len(range(df_openbis['identifier'])): - - props_dict = compute_openbis_sample_props_from_h5(df_openbis, df_h5, sample_idx) - sample_path_identifier = props_dict['identifier'] #path-like index - sample = sample_collection[sample_path_identifier] - - for prop in props_dict.keys(): - if prop in props_include_list: - sample.props[prop] = props_dict[prop] - - trans.add(sample) - - trans.commit() - - return 0 - -def conduct_dataframe_preprocessing_steps(df_h5, df_openbis): - - if not 'lastModifiedDatestr'in df_h5.columns: - raise ValueError('') - - df_h5, df_openbis = align_datetime_observation_windows(df_h5, df_openbis, 'lastModifiedDatestr' , 'registrationDate') - df_openbis = pair_openbis_and_h5_dataframes(df_openbis, df_h5, 'REFORMATED_FILENUMBER', 'name') - - return df_h5, df_openbis - - +import pandas as pd +import logging +import os +import datetime +from pybis import Openbis +import hidden + +admissible_props_list = ['$name', 'filenumber', 'default_experiment.experimental_results', + 'dataquality', '$xmlcomments', '$annotations_state', + 'sample_name', 'position_x', 'position_y', 'position_z', 'temp', 'cell_pressure', 'gas_flow_setting', 'sample_notes', + 'beamline', 'photon_energy', 'slit_entrance_v', 'slit_exit_v', 'izero', + 'slit_exit_h', 'hos', 'cone', 'endstation', 'hof', + 'method_name', 'region', 'lens_mode', 'acq_mode', 'dwell_time', 'frames', 'passenergy', + 'iterations', 'sequenceiterations', 'ke_range_center', 'ke_step'] + + +def initialize_openbis_obj(): + + # TODO: implement a more secure authentication method. + openbis_obj = Openbis('https://openbis-psi.labnotebook.ch/openbis/webapp/eln-lims/?menuUniqueId=null&viewName=showBlancPage&viewData=null', verify_certificates=False) + openbis_obj.login(hidden.username,hidden.password) + + return openbis_obj + +def align_datetime_observation_windows(df_h5: pd.DataFrame, df_openbis: pd.DataFrame, h5_datetime_var: str = 'lastModifiedDatestr', ob_datetime_var: str = 'registrationDate') -> pd.DataFrame: + + """ returns filtered/reduced versions of 'df' and 'df_ref' with aligned datetime observation windows. + That is, the datetime variable range is the same for the returned dataframes.""" + #""returns a filtered or reduced version of 'df' by removing all rows that are outside the datetime variable overlapping region between 'df' and 'df_ref'. + #""" + + #df_h5['lastModifiedDatestr'] = df_h5['lastModifiedDatestr'].astype('datetime64[ns]') + #df_h5 = df_h5.sort_values(by='lastModifiedDatestr') + + if not (h5_datetime_var in df_h5.columns.to_list() and ob_datetime_var in df_openbis.columns.to_list()): + #TODO: Check if ValueError is the best type of error to raise here + raise ValueError("Dataframes 'df' and 'df_ref' must contain columns 'datetime_var' and 'datetime_var_ref', storing values in suitable datetime string format (e.g., yyyy-mm-dd hh:mm:ss).") + + df_h5[h5_datetime_var] = df_h5[h5_datetime_var].astype('datetime64[ns]') + df_openbis[ob_datetime_var] = df_openbis[ob_datetime_var].astype('datetime64[ns]') + + min_timestamp = max([df_openbis[ob_datetime_var].min(), df_h5[h5_datetime_var].min()]) + max_timestamp = min([df_openbis[ob_datetime_var].max(), df_h5[h5_datetime_var].max()]) + + # Determine overlap between df and df_ref, and filters out all rows from df with datetime variable outside the overlapping datetime region. + datetime_overlap_indicator = (df_h5[h5_datetime_var] >= min_timestamp) & (df_h5[h5_datetime_var] <= max_timestamp) + df_h5 = df_h5.loc[datetime_overlap_indicator,:] + + datetime_overlap_indicator = (df_openbis[ob_datetime_var] >= min_timestamp) & (df_openbis[ob_datetime_var] <= max_timestamp) + df_openbis = df_openbis.loc[datetime_overlap_indicator,:] + + df_h5 = df_h5.sort_values(by=h5_datetime_var) + df_openbis = df_openbis.sort_values(by=ob_datetime_var) + + return df_h5, df_openbis + +def reformat_openbis_dataframe_filenumber(df_openbis): + + if not 'FILENUMBER' in df_openbis.columns: + raise ValueError('df_openbis does not contain the column "FILENUMBER". Make sure you query (e.g., o.get_samples(props=["filenumbe"])) that before creating df_openbis.') + #if not 'name' in df.columns: + # raise ValueError("df does not contain the column 'name'. Ensure df complies with Throsten's Table's format.") + + # Augment df_openbis with 'name' column consitent with Thorsten's naming convention + name_list = ['0' + item.zfill(3) + item.zfill(3) for item in df_openbis['FILENUMBER']] + df_openbis['REFORMATED_FILENUMBER'] = pd.Series(name_list, index=df_openbis.index) + + return df_openbis + +def pair_openbis_and_h5_dataframes(df_openbis, df_h5, pairing_ob_var: str, pairing_h5_var: str): + + """ Pairs every row (or openbis sample) in 'df_openbis' with a set of rows (or measurements) in 'df_h5' by matching the i-th row in 'df_h5' + with the rows of 'df_h5' that satisfy the string df_openbis.loc[i,pairing_var_1] is contained in the string df_h5[i,pairing_var_2] + + Example: pairing_var_1, pairing_var_2 = reformated 'REFORMATED_FILENUMBER', 'name' + + """ + # Reformat openbis dataframe filenumber so that it can be used to find associated measurements in h5 dataframe + df_openbis = reformat_openbis_dataframe_filenumber(df_openbis) + + related_indices_list = [] + for sample_idx in df_openbis.index: + sample_value = df_openbis.loc[sample_idx,pairing_ob_var] + tmp_list = [sample_value in item[0:item.find('_')] for item in df_h5[pairing_h5_var]] + related_indices_list.append(df_h5.index[tmp_list]) + + print('Paring openbis sample: ' + df_openbis.loc[sample_idx,pairing_ob_var]) + print('with reformated FILENUMBER: ' + sample_value) + print('to following measurements in h5 dataframe:') + print(df_h5.loc[df_h5.index[tmp_list],'name']) + print('\n') + + df_openbis['related_h5_indices'] = pd.Series(related_indices_list, index=df_openbis.index) + + return df_openbis + + +def range_cols_2_string(df,lb_var,ub_var): + + if not sum(df.loc[:,ub_var]-df.loc[:,lb_var])==0: + #tmp_list = ['-'.join([str(round(df.loc[i,lb_var],2)),str(round(df.loc[i,ub_var],1))]) for i in df.index] + tmp_list = ['-'.join(["{:.1f}".format(df.loc[i,lb_var]),"{:.1f}".format(df.loc[i,ub_var])]) for i in df.index] + elif len(df.loc[:,lb_var].unique())>1: # check if values are different + #tmp_list = [str(round(df.loc[i,lb_var],2)) for i in df.index] + tmp_list = ["{:.1f}".format(df.loc[i,lb_var]) for i in df.index] + else: + #tmp_list = [str(round(df.loc[0,lb_var],2))] + tmp_list = ["{:.1f}".format(df.loc[0,lb_var])] + return '/'.join(tmp_list) + +def col_2_string(df,column_var): + + if not column_var in df.columns: + raise ValueError("'column var must belong in df.columns") + + #tmp_list = [str(round(item,1)) for item in df[column_var]] + tmp_list = ["{:.2f}".format(item) for item in df[column_var]] + if len(df[column_var].unique())==1: + tmp_list = [tmp_list[0]] + + return '/'.join(tmp_list) + + +def compute_openbis_sample_props_from_h5(df_openbis, df_h5, sample_idx): + + prop2attr = {'sample_name':'sample', # ask Throsten whether this assignment is correct or not + 'position_x':'smplX_mm', + 'position_y':'smplY_mm', + 'position_z':'smplZ_mm', + 'temp':'sampleTemp_dC', + 'cell_pressure':'cellPressure_mbar', + #'gas_flow_setting': '', + 'method_name':'regionName', # measurement type: XPS or NEXAFS + 'region':'regionName', # VB/N1s/C1s + 'passenergy':'regionName', # REAL + + 'photon_energy':'xRayEkinRange_eV', + 'dwell_time':'scientaDwellTime_ms', + 'acq_mode':'scientaAcquisitionMode', + 'ke_range_center':'scientaEkinRange_eV', + 'ke_step':'scientaEkinStep_eV', + 'lens_mode':'scientaLensMode' + } + + sample_identifier = df_openbis.loc[sample_idx,'identifier'] + props_dict = {'FILENUMBER' : df_openbis.loc[sample_idx,'FILENUMBER']} + + #props_dict = {} + + if not len(df_openbis.loc[sample_idx,'related_h5_indices']): + props_dict['identifier'] = sample_identifier + return props_dict + + reduced_df_h5 = df_h5.loc[df_openbis.loc[sample_idx,'related_h5_indices'],:] + reduced_df_h5 = reduced_df_h5.reset_index() + + # include related_samples key for validation purposes. Related samples are used to compute average and/or combined openbis properties. + related_sample_list = [reduced_df_h5['name'][index] for index in reduced_df_h5['name'].index] + related_samples = ' / '.join(related_sample_list) + props_dict['Subject_samples'] = related_samples + + props_dict['sample_name'] = reduced_df_h5['sample'].unique()[0] if len(reduced_df_h5['sample'].unique())==1 else '/'.join(reduced_df_h5['sample'].tolist()) + + if not 'NEXAFS' in reduced_df_h5['regionName'].iloc[0]: + props_dict['identifier'] = sample_identifier + props_dict['method_name'] = 'XPS' + for item_idx in reduced_df_h5.index: + item = reduced_df_h5.loc[item_idx,'regionName'] + if item_idx > 0: + props_dict['region'] = props_dict['region'] + '/' + item[0:item.find('_')] + #props_dict['dwell_time'] = props_dict['dwell_time'] + '/' + str(reduced_df_h5.loc[item_idx,'scientaDwellTime_ms']) + #props_dict['ke_range_center'] = props_dict['ke_range_center'] + '/' + str(round(reduced_df_h5.loc[item_idx,['scientaEkinRange_eV_1','scientaEkinRange_eV_2']].mean(),2)) + #props_dict['ke_step_center'] = props_dict['ke_step_center'] + '/' + str(reduced_df_h5.loc[item_idx,'scientaEkinStep_eV']) + #props_dict['passenergy'].append(float(item[item.find('_')+1:item.find('eV')])) + else: + props_dict['region'] = item[0:item.find('_')] + #props_dict['dwell_time'] = str(reduced_df_h5.loc[item_idx,'scientaDwellTime_ms']) + #props_dict['ke_range_center'] = str(round(reduced_df_h5.loc[item_idx,['scientaEkinRange_eV_1','scientaEkinRange_eV_2']].mean(),2)) + #props_dict['ke_step_center'] = str(reduced_df_h5.loc[item_idx,'scientaEkinStep_eV']) + + #props_dict['passenergy'] = reduced_df_h5.loc[:,'scientaPassEnergy_eV'].min() + + else: + props_dict = {'identifier':sample_identifier,'method_name':'NEXAFS'} + + + #props_dict['temp'] = round(reduced_df_h5['sampleTemp_dC'].mean(),2) + #props_dict['cell_pressure'] = round(reduced_df_h5['cellPressure_mbar'].mean(),2) + props_dict['temp'] = "{:.2f}".format(reduced_df_h5['sampleTemp_dC'].mean()) + props_dict['cell_pressure'] = "{:.2f}".format(reduced_df_h5['cellPressure_mbar'].mean()) + + reduced_df_h5['scientaDwellTime_ms'] = reduced_df_h5['scientaDwellTime_ms']*1e-3 # covert ms to seconds + props_dict['dwell_time'] = col_2_string(reduced_df_h5,'scientaDwellTime_ms') + props_dict['passenergy'] = col_2_string(reduced_df_h5,'scientaPassEnergy_eV') + props_dict['ke_step_center'] = col_2_string(reduced_df_h5,'scientaEkinStep_eV') + #props_dict['photon_energy'] =round(reduced_df_h5[['xRayEkinRange_eV_1','xRayEkinRange_eV_2']].mean(axis=1)[0],2) + props_dict['photon_energy'] = range_cols_2_string(reduced_df_h5,'xRayEkinRange_eV_1','xRayEkinRange_eV_2') + props_dict['ke_range_center'] = range_cols_2_string(reduced_df_h5,'scientaEkinRange_eV_1','scientaEkinRange_eV_2') + + props_dict['lens_mode'] = reduced_df_h5['scientaLensMode'][0] + props_dict['acq_mode'] = reduced_df_h5['scientaAcquisitionMode'][0] + + props_dict['position_x'] = "{:.2f}".format(reduced_df_h5.loc[:,'smplX_mm'].mean()) # round(reduced_df_h5.loc[:,'smplX_mm'].mean(),2) + props_dict['position_y'] = "{:.2f}".format(reduced_df_h5.loc[:,'smplY_mm'].mean()) + props_dict['position_z'] = "{:.2f}".format(reduced_df_h5.loc[:,'smplZ_mm'].mean()) + + + + return props_dict + + + +def single_sample_update(sample_props_dict,sample_collection,props_include_list): + + """ Updates sample in openbis database specified in sample_props_dict, which must belong in sample_collection (i.e., result of openbis_obj.get_samples(...)) """ + + try: + sample_path_identifier = sample_props_dict['identifier'] #path-like index + sample = sample_collection[sample_path_identifier] + for prop in sample_props_dict.keys(): + if (prop in admissible_props_list) and (prop in props_include_list): + sample.props[prop] = sample_props_dict[prop] + sample.save() + except Exception: + logging.error(Exception) + + return 0 + + +def sample_batch_update(openbis_obj,sample_collection,df_openbis,df_h5,props_include_list): + + """ See """ + + if not 'related_h5_indices' in df_openbis.columns: + raise ValueError("Input dataframe 'df_openbis' must contain a column named 'related_h5_indeces', resulting from suitable proprocessing steps.") + + # TODO: as a safeguard, create exclude list containing properties that must not be changed + exclude_list = ['filenumber','FILENUMBER','identifier'] + for item in props_include_list: + if item in exclude_list: + props_include_list.remove(item) + + trans = openbis_obj.new_transaction() + for sample_idx in len(range(df_openbis['identifier'])): + + props_dict = compute_openbis_sample_props_from_h5(df_openbis, df_h5, sample_idx) + sample_path_identifier = props_dict['identifier'] #path-like index + sample = sample_collection[sample_path_identifier] + + for prop in props_dict.keys(): + if prop in props_include_list: + sample.props[prop] = props_dict[prop] + + trans.add(sample) + + trans.commit() + + return 0 + +def conduct_dataframe_preprocessing_steps(df_h5, df_openbis): + + if not 'lastModifiedDatestr'in df_h5.columns: + raise ValueError('') + + df_h5, df_openbis = align_datetime_observation_windows(df_h5, df_openbis, 'lastModifiedDatestr' , 'registrationDate') + df_openbis = pair_openbis_and_h5_dataframes(df_openbis, df_h5, 'REFORMATED_FILENUMBER', 'name') + + return df_h5, df_openbis + + diff --git a/src/utils_bge.py b/src/utils_bge.py index c350ebf..22053d2 100644 --- a/src/utils_bge.py +++ b/src/utils_bge.py @@ -1,58 +1,58 @@ -import scipy.optimize as sp_opt -import pandas as pd - - - -def construct_mask(x, subinterval_list): - - """ constructs a mask of length len(x) that indicates whether the entries of x lie within the subintervals, - speficified in the subinterval_list. - - Parameters: - x (array_like): - subinterval_list (list of two-element tuples): - - Returns: - mask (Bool array_like): - - Usage: - - x = np.array([0.0 0.25 0.5 0.75 1.5 2.0 2.5 3.0 3.5 4.0]) - subinterval_list = [(0.25,0.75),(2.5,3.5)] - mask = contruct_mask(x,subinterval_list) - - """ - - mask = x < x.min() - for subinterval in subinterval_list: - mask = mask | ((x >= subinterval[0]) & (x <= subinterval[1])) - - return mask - - -def estimate_background(x,y,mask,method: str): - - """fits a background model based on the values of x and y indicated by a mask using a method, among available options. - - Parameters: - x,y (array_like, e.g., np.array, pd.Series): - mask (Bool array_like): - method (str): - - Returns: - y_bg (array_like): values of the fitted model at x, or similarly the obtained background estimate - - """ - - if method == 'linear': - def linear_model(x,m,b): - return (m*x) + b - - popt, pcov = sp_opt.curve_fit(linear_model,x[mask],y[mask]) - - y_bg = linear_model(x,*popt) - - else: - raise ValueError("Parameter 'method' can only be set as 'linear'. Future code releases may include more options. ") - - return y_bg +import scipy.optimize as sp_opt +import pandas as pd + + + +def construct_mask(x, subinterval_list): + + """ constructs a mask of length len(x) that indicates whether the entries of x lie within the subintervals, + speficified in the subinterval_list. + + Parameters: + x (array_like): + subinterval_list (list of two-element tuples): + + Returns: + mask (Bool array_like): + + Usage: + + x = np.array([0.0 0.25 0.5 0.75 1.5 2.0 2.5 3.0 3.5 4.0]) + subinterval_list = [(0.25,0.75),(2.5,3.5)] + mask = contruct_mask(x,subinterval_list) + + """ + + mask = x < x.min() + for subinterval in subinterval_list: + mask = mask | ((x >= subinterval[0]) & (x <= subinterval[1])) + + return mask + + +def estimate_background(x,y,mask,method: str): + + """fits a background model based on the values of x and y indicated by a mask using a method, among available options. + + Parameters: + x,y (array_like, e.g., np.array, pd.Series): + mask (Bool array_like): + method (str): + + Returns: + y_bg (array_like): values of the fitted model at x, or similarly the obtained background estimate + + """ + + if method == 'linear': + def linear_model(x,m,b): + return (m*x) + b + + popt, pcov = sp_opt.curve_fit(linear_model,x[mask],y[mask]) + + y_bg = linear_model(x,*popt) + + else: + raise ValueError("Parameter 'method' can only be set as 'linear'. Future code releases may include more options. ") + + return y_bg diff --git a/utils/g5505_utils.py b/utils/g5505_utils.py index 53c53fd..38aeac8 100644 --- a/utils/g5505_utils.py +++ b/utils/g5505_utils.py @@ -1,403 +1,403 @@ -import pandas as pd -import os -import sys -import shutil -import datetime -import logging -import numpy as np -import h5py -import re - - -def setup_logging(log_dir, log_filename): - """Sets up logging to a specified directory and file. - - Parameters: - log_dir (str): Directory to save the log file. - log_filename (str): Name of the log file. - """ - # Ensure the log directory exists - os.makedirs(log_dir, exist_ok=True) - - # Create a logger instance - logger = logging.getLogger() - logger.setLevel(logging.INFO) - - # Create a file handler - log_path = os.path.join(log_dir, log_filename) - file_handler = logging.FileHandler(log_path) - - # Create a formatter and set it for the handler - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') - file_handler.setFormatter(formatter) - - # Add the handler to the logger - logger.addHandler(file_handler) - - -def is_callable_list(x : list): - return all([callable(item) for item in x]) - -def is_str_list(x : list): - return all([isinstance(item,str) for item in x]) - -def augment_with_filetype(df): - df['filetype'] = [os.path.splitext(item)[1][1::] for item in df['filename']] - #return [os.path.splitext(item)[1][1::] for item in df['filename']] - return df - -def augment_with_filenumber(df): - df['filenumber'] = [item[0:item.find('_')] for item in df['filename']] - #return [item[0:item.find('_')] for item in df['filename']] - return df - -def group_by_df_column(df, column_name: str): - """ - df (pandas.DataFrame): - column_name (str): column_name of df by which grouping operation will take place. - """ - - if not column_name in df.columns: - raise ValueError("column_name must be in the columns of df.") - - return df[column_name] - -def split_sample_col_into_sample_and_data_quality_cols(input_data: pd.DataFrame): - - sample_name = [] - sample_quality = [] - for item in input_data['sample']: - if item.find('(')!=-1: - #print(item) - sample_name.append(item[0:item.find('(')]) - sample_quality.append(item[item.find('(')+1:len(item)-1]) - else: - if item=='': - sample_name.append('Not yet annotated') - sample_quality.append('unevaluated') - else: - sample_name.append(item) - sample_quality.append('good data') - input_data['sample'] = sample_name - input_data['data_quality'] = sample_quality - - return input_data - -def make_file_copy(source_file_path, output_folder_name : str = 'tmp_files'): - - pathtail, filename = os.path.split(source_file_path) - #backup_filename = 'backup_'+ filename - backup_filename = filename - # Path - ROOT_DIR = os.path.abspath(os.curdir) - - tmp_dirpath = os.path.join(ROOT_DIR,output_folder_name) - if not os.path.exists(tmp_dirpath): - os.mkdir(tmp_dirpath) - - tmp_file_path = os.path.join(tmp_dirpath,backup_filename) - shutil.copy(source_file_path, tmp_file_path) - - return tmp_file_path - -def created_at(datetime_format = '%Y-%m-%d %H:%M:%S'): - now = datetime.datetime.now() - # Populate now object with time zone information obtained from the local system - now_tz_aware = now.astimezone() - tz = now_tz_aware.strftime('%z') - # Replace colons in the time part of the timestamp with hyphens to make it file name friendly - created_at = now_tz_aware.strftime(datetime_format) #+ '_UTC-OFST_' + tz - return created_at - -def sanitize_dataframe(df: pd.DataFrame) -> pd.DataFrame: - # Handle datetime columns (convert to string in 'yyyy-mm-dd hh:mm:ss' format) - datetime_cols = df.select_dtypes(include=['datetime']).columns - for col in datetime_cols: - # Convert datetime to string in the specified format, handling NaT - df[col] = df[col].dt.strftime('%Y-%m-%d %H-%M-%S') - - # Handle object columns with mixed types - otype_cols = df.select_dtypes(include='O') - for col in otype_cols: - col_data = df[col] - - # Check if all elements in the column are strings - if col_data.apply(lambda x: isinstance(x, str)).all(): - df[col] = df[col].astype(str) - else: - # If the column contains mixed types, attempt to convert to numeric, coercing errors to NaN - df[col] = pd.to_numeric(col_data, errors='coerce') - - # Handle NaN values differently based on dtype - if pd.api.types.is_string_dtype(df[col]): - # Replace NaN in string columns with empty string - df[col] = df[col].fillna('') # Replace NaN with empty string - elif pd.api.types.is_numeric_dtype(df[col]): - # For numeric columns, we want to keep NaN as it is - # But if integer column has NaN, consider casting to float - if pd.api.types.is_integer_dtype(df[col]): - df[col] = df[col].astype(float) # Cast to float to allow NaN - else: - df[col] = df[col].fillna(np.nan) # Keep NaN in float columns - - return df - -def convert_dataframe_to_np_structured_array(df: pd.DataFrame): - - df = sanitize_dataframe(df) - # Define the dtype for the structured array, ensuring compatibility with h5py - dtype = [] - for col in df.columns: - - col_data = df[col] - col_dtype = col_data.dtype - - try: - if pd.api.types.is_string_dtype(col_dtype): - # Convert string dtype to fixed-length strings - max_len = col_data.str.len().max() if not col_data.isnull().all() else 0 - dtype.append((col, f'S{max_len}')) - elif pd.api.types.is_integer_dtype(col_dtype): - dtype.append((col, 'i4')) # Assuming 32-bit integer - elif pd.api.types.is_float_dtype(col_dtype): - dtype.append((col, 'f4')) # Assuming 32-bit float - else: - # Handle unsupported data types - print(f"Unsupported dtype found in column '{col}': {col_data.dtype}") - raise ValueError(f"Unsupported data type: {col_data.dtype}") - - except Exception as e: - # Log more detailed error message - print(f"Error processing column '{col}': {e}") - raise - - # Convert the DataFrame to a structured array - structured_array = np.array(list(df.itertuples(index=False, name=None)), dtype=dtype) - - return structured_array - -def convert_string_to_bytes(input_list: list): - """Convert a list of strings into a numpy array with utf8-type entries. - - Parameters - ---------- - input_list (list) : list of string objects - - Returns - ------- - input_array_bytes (ndarray): array of ut8-type entries. - """ - utf8_type = lambda max_length: h5py.string_dtype('utf-8', max_length) - if input_list: - max_length = max(len(item) for item in input_list) - # Convert the strings to bytes with utf-8 encoding, specifying errors='ignore' to skip characters that cannot be encoded - input_list_bytes = [item.encode('utf-8', errors='ignore') for item in input_list] - input_array_bytes = np.array(input_list_bytes,dtype=utf8_type(max_length)) - else: - input_array_bytes = np.array([],dtype=utf8_type(0)) - - return input_array_bytes - -def convert_attrdict_to_np_structured_array(attr_value: dict): - """ - Converts a dictionary of attributes into a numpy structured array for HDF5 - compound type compatibility. - - Each dictionary key is mapped to a field in the structured array, with the - data type (S) determined by the longest string representation of the values. - If the dictionary is empty, the function returns 'missing'. - - Parameters - ---------- - attr_value : dict - Dictionary containing the attributes to be converted. Example: - attr_value = { - 'name': 'Temperature', - 'unit': 'Celsius', - 'value': 23.5, - 'timestamp': '2023-09-26 10:00' - } - - Returns - ------- - new_attr_value : ndarray or str - Numpy structured array with UTF-8 encoded fields. Returns 'missing' if - the input dictionary is empty. - """ - dtype = [] - values_list = [] - max_length = max(len(str(attr_value[key])) for key in attr_value.keys()) - for key in attr_value.keys(): - if key != 'rename_as': - dtype.append((key, f'S{max_length}')) - values_list.append(attr_value[key]) - if values_list: - new_attr_value = np.array([tuple(values_list)], dtype=dtype) - else: - new_attr_value = 'missing' - - return new_attr_value - - -def infer_units(column_name): - # TODO: complete or remove - - match = re.search('\[.+\]') - - if match: - return match - else: - match = re.search('\(.+\)') - - return match - -def progressBar(count_value, total, suffix=''): - bar_length = 100 - filled_up_Length = int(round(bar_length* count_value / float(total))) - percentage = round(100.0 * count_value/float(total),1) - bar = '=' * filled_up_Length + '-' * (bar_length - filled_up_Length) - sys.stdout.write('[%s] %s%s ...%s\r' %(bar, percentage, '%', suffix)) - sys.stdout.flush() - -def copy_directory_with_contraints(input_dir_path, output_dir_path, - select_dir_keywords = None, - select_file_keywords = None, - allowed_file_extensions = None, - dry_run = False): - """ - Copies files from input_dir_path to output_dir_path based on specified constraints. - - Parameters - ---------- - input_dir_path (str): Path to the input directory. - output_dir_path (str): Path to the output directory. - select_dir_keywords (list): optional, List of keywords for selecting directories. - select_file_keywords (list): optional, List of keywords for selecting files. - allowed_file_extensions (list): optional, List of allowed file extensions. - - Returns - ------- - path_to_files_dict (dict): dictionary mapping directory paths to lists of copied file names satisfying the constraints. - """ - - # Unconstrained default behavior: No filters, make sure variable are lists even when defined as None in function signature - select_dir_keywords = select_dir_keywords or [] - select_file_keywords = select_file_keywords or [] - allowed_file_extensions = allowed_file_extensions or [] - - date = created_at('%Y_%m').replace(":", "-") - log_dir='logs/' - setup_logging(log_dir, f"copy_directory_with_contraints_{date}.log") - - # Define helper functions. Return by default true when filtering lists are either None or [] - def has_allowed_extension(filename): - return not allowed_file_extensions or os.path.splitext(filename)[1] in allowed_file_extensions - - def file_is_selected(filename): - return not select_file_keywords or any(keyword in filename for keyword in select_file_keywords) - - - # Collect paths of directories, which are directly connected to the root dir and match select_dir_keywords - paths = [] - if select_dir_keywords: - for item in os.listdir(input_dir_path): #Path(input_dir_path).iterdir(): - if any([item in keyword for keyword in select_dir_keywords]): - paths.append(os.path.join(input_dir_path,item)) - else: - paths.append(input_dir_path) #paths.append(Path(input_dir_path)) - - - path_to_files_dict = {} # Dictionary to store directory-file pairs satisfying constraints - - for subpath in paths: - - for dirpath, _, filenames in os.walk(subpath,topdown=False): - - # Reduce filenames to those that are admissible - admissible_filenames = [ - filename for filename in filenames - if file_is_selected(filename) and has_allowed_extension(filename) - ] - - if admissible_filenames: # Only create directory if there are files to copy - - relative_dirpath = os.path.relpath(dirpath, input_dir_path) - target_dirpath = os.path.join(output_dir_path, relative_dirpath) - path_to_files_dict[target_dirpath] = admissible_filenames - - if not dry_run: - - # Perform the actual copying - - os.makedirs(target_dirpath, exist_ok=True) - - for filename in admissible_filenames: - src_file_path = os.path.join(dirpath, filename) - dest_file_path = os.path.join(target_dirpath, filename) - try: - shutil.copy2(src_file_path, dest_file_path) - except Exception as e: - logging.error("Failed to copy %s: %s", src_file_path, e) - - return path_to_files_dict - -def to_serializable_dtype(value): - - """Transform value's dtype into YAML/JSON compatible dtype - - Parameters - ---------- - value : _type_ - _description_ - - Returns - ------- - _type_ - _description_ - """ - try: - if isinstance(value, np.generic): - if np.issubdtype(value.dtype, np.bytes_): - value = value.decode('utf-8') - elif np.issubdtype(value.dtype, np.unicode_): - value = str(value) - elif np.issubdtype(value.dtype, np.number): - value = float(value) - else: - print('Yaml-compatible data-type was not found. Value has been set to NaN.') - value = np.nan - elif isinstance(value, np.ndarray): - # Handling structured array types (with fields) - if value.dtype.names: - value = {field: to_serializable_dtype(value[field]) for field in value.dtype.names} - else: - # Handling regular array NumPy types with assumption of unform dtype accross array elements - # TODO: evaluate a more general way to check for individual dtypes - if isinstance(value[0], bytes): - # Decode bytes - value = [item.decode('utf-8') for item in value] if len(value) > 1 else value[0].decode('utf-8') - elif isinstance(value[0], str): - # Already a string type - value = [str(item) for item in value] if len(value) > 1 else str(value[0]) - elif isinstance(value[0], int): - # Integer type - value = [int(item) for item in value] if len(value) > 1 else int(value[0]) - elif isinstance(value[0], float): - # Floating type - value = [float(item) for item in value] if len(value) > 1 else float(value[0]) - else: - print('Yaml-compatible data-type was not found. Value has been set to NaN.') - print("Debug: value.dtype is", value.dtype) - value = np.nan - - except Exception as e: - print(f'Error converting value: {e}. Value has been set to NaN.') - value = np.nan - - return value - -def is_structured_array(attr_val): - if isinstance(attr_val,np.ndarray): - return True if attr_val.dtype.names is not None else False - else: +import pandas as pd +import os +import sys +import shutil +import datetime +import logging +import numpy as np +import h5py +import re + + +def setup_logging(log_dir, log_filename): + """Sets up logging to a specified directory and file. + + Parameters: + log_dir (str): Directory to save the log file. + log_filename (str): Name of the log file. + """ + # Ensure the log directory exists + os.makedirs(log_dir, exist_ok=True) + + # Create a logger instance + logger = logging.getLogger() + logger.setLevel(logging.INFO) + + # Create a file handler + log_path = os.path.join(log_dir, log_filename) + file_handler = logging.FileHandler(log_path) + + # Create a formatter and set it for the handler + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + file_handler.setFormatter(formatter) + + # Add the handler to the logger + logger.addHandler(file_handler) + + +def is_callable_list(x : list): + return all([callable(item) for item in x]) + +def is_str_list(x : list): + return all([isinstance(item,str) for item in x]) + +def augment_with_filetype(df): + df['filetype'] = [os.path.splitext(item)[1][1::] for item in df['filename']] + #return [os.path.splitext(item)[1][1::] for item in df['filename']] + return df + +def augment_with_filenumber(df): + df['filenumber'] = [item[0:item.find('_')] for item in df['filename']] + #return [item[0:item.find('_')] for item in df['filename']] + return df + +def group_by_df_column(df, column_name: str): + """ + df (pandas.DataFrame): + column_name (str): column_name of df by which grouping operation will take place. + """ + + if not column_name in df.columns: + raise ValueError("column_name must be in the columns of df.") + + return df[column_name] + +def split_sample_col_into_sample_and_data_quality_cols(input_data: pd.DataFrame): + + sample_name = [] + sample_quality = [] + for item in input_data['sample']: + if item.find('(')!=-1: + #print(item) + sample_name.append(item[0:item.find('(')]) + sample_quality.append(item[item.find('(')+1:len(item)-1]) + else: + if item=='': + sample_name.append('Not yet annotated') + sample_quality.append('unevaluated') + else: + sample_name.append(item) + sample_quality.append('good data') + input_data['sample'] = sample_name + input_data['data_quality'] = sample_quality + + return input_data + +def make_file_copy(source_file_path, output_folder_name : str = 'tmp_files'): + + pathtail, filename = os.path.split(source_file_path) + #backup_filename = 'backup_'+ filename + backup_filename = filename + # Path + ROOT_DIR = os.path.abspath(os.curdir) + + tmp_dirpath = os.path.join(ROOT_DIR,output_folder_name) + if not os.path.exists(tmp_dirpath): + os.mkdir(tmp_dirpath) + + tmp_file_path = os.path.join(tmp_dirpath,backup_filename) + shutil.copy(source_file_path, tmp_file_path) + + return tmp_file_path + +def created_at(datetime_format = '%Y-%m-%d %H:%M:%S'): + now = datetime.datetime.now() + # Populate now object with time zone information obtained from the local system + now_tz_aware = now.astimezone() + tz = now_tz_aware.strftime('%z') + # Replace colons in the time part of the timestamp with hyphens to make it file name friendly + created_at = now_tz_aware.strftime(datetime_format) #+ '_UTC-OFST_' + tz + return created_at + +def sanitize_dataframe(df: pd.DataFrame) -> pd.DataFrame: + # Handle datetime columns (convert to string in 'yyyy-mm-dd hh:mm:ss' format) + datetime_cols = df.select_dtypes(include=['datetime']).columns + for col in datetime_cols: + # Convert datetime to string in the specified format, handling NaT + df[col] = df[col].dt.strftime('%Y-%m-%d %H-%M-%S') + + # Handle object columns with mixed types + otype_cols = df.select_dtypes(include='O') + for col in otype_cols: + col_data = df[col] + + # Check if all elements in the column are strings + if col_data.apply(lambda x: isinstance(x, str)).all(): + df[col] = df[col].astype(str) + else: + # If the column contains mixed types, attempt to convert to numeric, coercing errors to NaN + df[col] = pd.to_numeric(col_data, errors='coerce') + + # Handle NaN values differently based on dtype + if pd.api.types.is_string_dtype(df[col]): + # Replace NaN in string columns with empty string + df[col] = df[col].fillna('') # Replace NaN with empty string + elif pd.api.types.is_numeric_dtype(df[col]): + # For numeric columns, we want to keep NaN as it is + # But if integer column has NaN, consider casting to float + if pd.api.types.is_integer_dtype(df[col]): + df[col] = df[col].astype(float) # Cast to float to allow NaN + else: + df[col] = df[col].fillna(np.nan) # Keep NaN in float columns + + return df + +def convert_dataframe_to_np_structured_array(df: pd.DataFrame): + + df = sanitize_dataframe(df) + # Define the dtype for the structured array, ensuring compatibility with h5py + dtype = [] + for col in df.columns: + + col_data = df[col] + col_dtype = col_data.dtype + + try: + if pd.api.types.is_string_dtype(col_dtype): + # Convert string dtype to fixed-length strings + max_len = col_data.str.len().max() if not col_data.isnull().all() else 0 + dtype.append((col, f'S{max_len}')) + elif pd.api.types.is_integer_dtype(col_dtype): + dtype.append((col, 'i4')) # Assuming 32-bit integer + elif pd.api.types.is_float_dtype(col_dtype): + dtype.append((col, 'f4')) # Assuming 32-bit float + else: + # Handle unsupported data types + print(f"Unsupported dtype found in column '{col}': {col_data.dtype}") + raise ValueError(f"Unsupported data type: {col_data.dtype}") + + except Exception as e: + # Log more detailed error message + print(f"Error processing column '{col}': {e}") + raise + + # Convert the DataFrame to a structured array + structured_array = np.array(list(df.itertuples(index=False, name=None)), dtype=dtype) + + return structured_array + +def convert_string_to_bytes(input_list: list): + """Convert a list of strings into a numpy array with utf8-type entries. + + Parameters + ---------- + input_list (list) : list of string objects + + Returns + ------- + input_array_bytes (ndarray): array of ut8-type entries. + """ + utf8_type = lambda max_length: h5py.string_dtype('utf-8', max_length) + if input_list: + max_length = max(len(item) for item in input_list) + # Convert the strings to bytes with utf-8 encoding, specifying errors='ignore' to skip characters that cannot be encoded + input_list_bytes = [item.encode('utf-8', errors='ignore') for item in input_list] + input_array_bytes = np.array(input_list_bytes,dtype=utf8_type(max_length)) + else: + input_array_bytes = np.array([],dtype=utf8_type(0)) + + return input_array_bytes + +def convert_attrdict_to_np_structured_array(attr_value: dict): + """ + Converts a dictionary of attributes into a numpy structured array for HDF5 + compound type compatibility. + + Each dictionary key is mapped to a field in the structured array, with the + data type (S) determined by the longest string representation of the values. + If the dictionary is empty, the function returns 'missing'. + + Parameters + ---------- + attr_value : dict + Dictionary containing the attributes to be converted. Example: + attr_value = { + 'name': 'Temperature', + 'unit': 'Celsius', + 'value': 23.5, + 'timestamp': '2023-09-26 10:00' + } + + Returns + ------- + new_attr_value : ndarray or str + Numpy structured array with UTF-8 encoded fields. Returns 'missing' if + the input dictionary is empty. + """ + dtype = [] + values_list = [] + max_length = max(len(str(attr_value[key])) for key in attr_value.keys()) + for key in attr_value.keys(): + if key != 'rename_as': + dtype.append((key, f'S{max_length}')) + values_list.append(attr_value[key]) + if values_list: + new_attr_value = np.array([tuple(values_list)], dtype=dtype) + else: + new_attr_value = 'missing' + + return new_attr_value + + +def infer_units(column_name): + # TODO: complete or remove + + match = re.search('\[.+\]') + + if match: + return match + else: + match = re.search('\(.+\)') + + return match + +def progressBar(count_value, total, suffix=''): + bar_length = 100 + filled_up_Length = int(round(bar_length* count_value / float(total))) + percentage = round(100.0 * count_value/float(total),1) + bar = '=' * filled_up_Length + '-' * (bar_length - filled_up_Length) + sys.stdout.write('[%s] %s%s ...%s\r' %(bar, percentage, '%', suffix)) + sys.stdout.flush() + +def copy_directory_with_contraints(input_dir_path, output_dir_path, + select_dir_keywords = None, + select_file_keywords = None, + allowed_file_extensions = None, + dry_run = False): + """ + Copies files from input_dir_path to output_dir_path based on specified constraints. + + Parameters + ---------- + input_dir_path (str): Path to the input directory. + output_dir_path (str): Path to the output directory. + select_dir_keywords (list): optional, List of keywords for selecting directories. + select_file_keywords (list): optional, List of keywords for selecting files. + allowed_file_extensions (list): optional, List of allowed file extensions. + + Returns + ------- + path_to_files_dict (dict): dictionary mapping directory paths to lists of copied file names satisfying the constraints. + """ + + # Unconstrained default behavior: No filters, make sure variable are lists even when defined as None in function signature + select_dir_keywords = select_dir_keywords or [] + select_file_keywords = select_file_keywords or [] + allowed_file_extensions = allowed_file_extensions or [] + + date = created_at('%Y_%m').replace(":", "-") + log_dir='logs/' + setup_logging(log_dir, f"copy_directory_with_contraints_{date}.log") + + # Define helper functions. Return by default true when filtering lists are either None or [] + def has_allowed_extension(filename): + return not allowed_file_extensions or os.path.splitext(filename)[1] in allowed_file_extensions + + def file_is_selected(filename): + return not select_file_keywords or any(keyword in filename for keyword in select_file_keywords) + + + # Collect paths of directories, which are directly connected to the root dir and match select_dir_keywords + paths = [] + if select_dir_keywords: + for item in os.listdir(input_dir_path): #Path(input_dir_path).iterdir(): + if any([item in keyword for keyword in select_dir_keywords]): + paths.append(os.path.join(input_dir_path,item)) + else: + paths.append(input_dir_path) #paths.append(Path(input_dir_path)) + + + path_to_files_dict = {} # Dictionary to store directory-file pairs satisfying constraints + + for subpath in paths: + + for dirpath, _, filenames in os.walk(subpath,topdown=False): + + # Reduce filenames to those that are admissible + admissible_filenames = [ + filename for filename in filenames + if file_is_selected(filename) and has_allowed_extension(filename) + ] + + if admissible_filenames: # Only create directory if there are files to copy + + relative_dirpath = os.path.relpath(dirpath, input_dir_path) + target_dirpath = os.path.join(output_dir_path, relative_dirpath) + path_to_files_dict[target_dirpath] = admissible_filenames + + if not dry_run: + + # Perform the actual copying + + os.makedirs(target_dirpath, exist_ok=True) + + for filename in admissible_filenames: + src_file_path = os.path.join(dirpath, filename) + dest_file_path = os.path.join(target_dirpath, filename) + try: + shutil.copy2(src_file_path, dest_file_path) + except Exception as e: + logging.error("Failed to copy %s: %s", src_file_path, e) + + return path_to_files_dict + +def to_serializable_dtype(value): + + """Transform value's dtype into YAML/JSON compatible dtype + + Parameters + ---------- + value : _type_ + _description_ + + Returns + ------- + _type_ + _description_ + """ + try: + if isinstance(value, np.generic): + if np.issubdtype(value.dtype, np.bytes_): + value = value.decode('utf-8') + elif np.issubdtype(value.dtype, np.unicode_): + value = str(value) + elif np.issubdtype(value.dtype, np.number): + value = float(value) + else: + print('Yaml-compatible data-type was not found. Value has been set to NaN.') + value = np.nan + elif isinstance(value, np.ndarray): + # Handling structured array types (with fields) + if value.dtype.names: + value = {field: to_serializable_dtype(value[field]) for field in value.dtype.names} + else: + # Handling regular array NumPy types with assumption of unform dtype accross array elements + # TODO: evaluate a more general way to check for individual dtypes + if isinstance(value[0], bytes): + # Decode bytes + value = [item.decode('utf-8') for item in value] if len(value) > 1 else value[0].decode('utf-8') + elif isinstance(value[0], str): + # Already a string type + value = [str(item) for item in value] if len(value) > 1 else str(value[0]) + elif isinstance(value[0], int): + # Integer type + value = [int(item) for item in value] if len(value) > 1 else int(value[0]) + elif isinstance(value[0], float): + # Floating type + value = [float(item) for item in value] if len(value) > 1 else float(value[0]) + else: + print('Yaml-compatible data-type was not found. Value has been set to NaN.') + print("Debug: value.dtype is", value.dtype) + value = np.nan + + except Exception as e: + print(f'Error converting value: {e}. Value has been set to NaN.') + value = np.nan + + return value + +def is_structured_array(attr_val): + if isinstance(attr_val,np.ndarray): + return True if attr_val.dtype.names is not None else False + else: return False \ No newline at end of file diff --git a/visualization/hdf5_vis.py b/visualization/hdf5_vis.py index 9ca2511..c1d8a22 100644 --- a/visualization/hdf5_vis.py +++ b/visualization/hdf5_vis.py @@ -1,69 +1,69 @@ -import sys -import os -root_dir = os.path.abspath(os.curdir) -sys.path.append(root_dir) - -import h5py -import yaml - -import numpy as np -import pandas as pd - -from plotly.subplots import make_subplots -import plotly.graph_objects as go -import plotly.express as px -#import plotly.io as pio -from src.hdf5_ops import get_parent_child_relationships - - - -def display_group_hierarchy_on_a_treemap(filename: str): - - """ - filename (str): hdf5 file's filename""" - - with h5py.File(filename,'r') as file: - nodes, parents, values = get_parent_child_relationships(file) - - metadata_list = [] - metadata_dict={} - for key in file.attrs.keys(): - #if 'metadata' in key: - if isinstance(file.attrs[key], str): # Check if the attribute is a string - metadata_key = key[key.find('_') + 1:] - metadata_value = file.attrs[key] - metadata_dict[metadata_key] = metadata_value - metadata_list.append(f'{metadata_key}: {metadata_value}') - - #metadata_dict[key[key.find('_')+1::]]= file.attrs[key] - #metadata_list.append(key[key.find('_')+1::]+':'+file.attrs[key]) - - metadata = '
'.join(['
'] + metadata_list) - - customdata_series = pd.Series(nodes) - customdata_series[0] = metadata - - fig = make_subplots(1, 1, specs=[[{"type": "domain"}]],) - fig.add_trace(go.Treemap( - labels=nodes, #formating_df['formated_names'][nodes], - parents=parents,#formating_df['formated_names'][parents], - values=values, - branchvalues='remainder', - customdata= customdata_series, - #marker=dict( - # colors=df_all_trees['color'], - # colorscale='RdBu', - # cmid=average_score), - #hovertemplate='%{label}
Number of files: %{value}
Success rate: %{color:.2f}', - hovertemplate='%{label}
Count: %{value}
Path: %{customdata}', - name='', - root_color="lightgrey" - )) - fig.update_layout(width = 800, height= 600, margin = dict(t=50, l=25, r=25, b=25)) - fig.show() - file_name, file_ext = os.path.splitext(filename) - fig.write_html(file_name + ".html") - - #pio.write_image(fig,file_name + ".png",width=800,height=600,format='png') - -# +import sys +import os +root_dir = os.path.abspath(os.curdir) +sys.path.append(root_dir) + +import h5py +import yaml + +import numpy as np +import pandas as pd + +from plotly.subplots import make_subplots +import plotly.graph_objects as go +import plotly.express as px +#import plotly.io as pio +from src.hdf5_ops import get_parent_child_relationships + + + +def display_group_hierarchy_on_a_treemap(filename: str): + + """ + filename (str): hdf5 file's filename""" + + with h5py.File(filename,'r') as file: + nodes, parents, values = get_parent_child_relationships(file) + + metadata_list = [] + metadata_dict={} + for key in file.attrs.keys(): + #if 'metadata' in key: + if isinstance(file.attrs[key], str): # Check if the attribute is a string + metadata_key = key[key.find('_') + 1:] + metadata_value = file.attrs[key] + metadata_dict[metadata_key] = metadata_value + metadata_list.append(f'{metadata_key}: {metadata_value}') + + #metadata_dict[key[key.find('_')+1::]]= file.attrs[key] + #metadata_list.append(key[key.find('_')+1::]+':'+file.attrs[key]) + + metadata = '
'.join(['
'] + metadata_list) + + customdata_series = pd.Series(nodes) + customdata_series[0] = metadata + + fig = make_subplots(1, 1, specs=[[{"type": "domain"}]],) + fig.add_trace(go.Treemap( + labels=nodes, #formating_df['formated_names'][nodes], + parents=parents,#formating_df['formated_names'][parents], + values=values, + branchvalues='remainder', + customdata= customdata_series, + #marker=dict( + # colors=df_all_trees['color'], + # colorscale='RdBu', + # cmid=average_score), + #hovertemplate='%{label}
Number of files: %{value}
Success rate: %{color:.2f}', + hovertemplate='%{label}
Count: %{value}
Path: %{customdata}', + name='', + root_color="lightgrey" + )) + fig.update_layout(width = 800, height= 600, margin = dict(t=50, l=25, r=25, b=25)) + fig.show() + file_name, file_ext = os.path.splitext(filename) + fig.write_html(file_name + ".html") + + #pio.write_image(fig,file_name + ".png",width=800,height=600,format='png') + +# diff --git a/visualization/napp_plotlib.py b/visualization/napp_plotlib.py index c22ca93..983a5a5 100644 --- a/visualization/napp_plotlib.py +++ b/visualization/napp_plotlib.py @@ -1,54 +1,54 @@ -import pandas as pd -import numpy as np -import matplotlib.pyplot as plt - -def plot_image(dataframe,filter): - - for meas_idx in dataframe.loc[filter,:].index: - meas = dataframe.loc[meas_idx,:] # pandas Series - fig = plt.figure() - ax = plt.gca() - rows, cols = meas['image'].shape - scientaEkin_eV = meas['scientaEkin_eV'].flatten() - x_min, x_max = np.min(scientaEkin_eV), np.max(scientaEkin_eV) - y_min, y_max = 0, rows - ax.imshow(meas['image'],extent = [x_min,x_max,y_min,y_max]) - ax.set_xlabel('scientaEkin_eV') - ax.set_ylabel('Replicates') - ax.set_title(meas['name'][0] + '\n' + meas['sample'][0]+ '\n' + meas['lastModifiedDatestr'][0]) - -def plot_spectra(dataframe,filter): - - """ plot_spectra plots XPS spectra associated to 'dataframe' after row reduced by 'filter'. - When more than one row are specified by the 'filter' input, indivial spectrum are superimposed - on the same plot. - - Parameters: - dataframe (pandas.DataFrame): table with heterogenous entries obtained by read_hdf5_as_dataframe.py. - filter (binaray array): binary indexing array with same number of entries as rows in dataframe. - - """ - fig = plt.figure() - ax = plt.gca() - - for meas_idx in dataframe.loc[filter,:].index: - meas = dataframe.loc[meas_idx,:] # pandas Series - - rows, cols = meas['image'].shape - bindingEnergy_eV = meas['bindingEnergy_eV'].flatten() - spectrum_countsPerSecond = meas['spectrum_countsPerSecond'].flatten() - x_min, x_max = np.min(bindingEnergy_eV), np.max(bindingEnergy_eV) - y_min, y_max = 0, rows - #for i in range(cols): - #ax.plot(bindingEnergy_eV, spectrum_countsPerSecond,label = meas['name'][0]) - ax.plot(bindingEnergy_eV, spectrum_countsPerSecond,label = meas['name']) - - ax.set_xlabel('bindingEnergy_eV') - ax.set_ylabel('counts Per Second') - ax.set_title('\n'+meas['sample']+ '\n' + 'PE spectra') - #ax.set_title('\n'+meas['sample'][0]+ '\n' + 'PE spectra') - #ax.set_title(meas['name'][0] + '\n'+meas['sample'][0]+ '\n' + meas['lastModifiedDatestr'][0]) - ax.legend() - - - +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt + +def plot_image(dataframe,filter): + + for meas_idx in dataframe.loc[filter,:].index: + meas = dataframe.loc[meas_idx,:] # pandas Series + fig = plt.figure() + ax = plt.gca() + rows, cols = meas['image'].shape + scientaEkin_eV = meas['scientaEkin_eV'].flatten() + x_min, x_max = np.min(scientaEkin_eV), np.max(scientaEkin_eV) + y_min, y_max = 0, rows + ax.imshow(meas['image'],extent = [x_min,x_max,y_min,y_max]) + ax.set_xlabel('scientaEkin_eV') + ax.set_ylabel('Replicates') + ax.set_title(meas['name'][0] + '\n' + meas['sample'][0]+ '\n' + meas['lastModifiedDatestr'][0]) + +def plot_spectra(dataframe,filter): + + """ plot_spectra plots XPS spectra associated to 'dataframe' after row reduced by 'filter'. + When more than one row are specified by the 'filter' input, indivial spectrum are superimposed + on the same plot. + + Parameters: + dataframe (pandas.DataFrame): table with heterogenous entries obtained by read_hdf5_as_dataframe.py. + filter (binaray array): binary indexing array with same number of entries as rows in dataframe. + + """ + fig = plt.figure() + ax = plt.gca() + + for meas_idx in dataframe.loc[filter,:].index: + meas = dataframe.loc[meas_idx,:] # pandas Series + + rows, cols = meas['image'].shape + bindingEnergy_eV = meas['bindingEnergy_eV'].flatten() + spectrum_countsPerSecond = meas['spectrum_countsPerSecond'].flatten() + x_min, x_max = np.min(bindingEnergy_eV), np.max(bindingEnergy_eV) + y_min, y_max = 0, rows + #for i in range(cols): + #ax.plot(bindingEnergy_eV, spectrum_countsPerSecond,label = meas['name'][0]) + ax.plot(bindingEnergy_eV, spectrum_countsPerSecond,label = meas['name']) + + ax.set_xlabel('bindingEnergy_eV') + ax.set_ylabel('counts Per Second') + ax.set_title('\n'+meas['sample']+ '\n' + 'PE spectra') + #ax.set_title('\n'+meas['sample'][0]+ '\n' + 'PE spectra') + #ax.set_title(meas['name'][0] + '\n'+meas['sample'][0]+ '\n' + meas['lastModifiedDatestr'][0]) + ax.legend() + + +