diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..b8de546 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,15 @@ +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +sphinx: + configuration: docs/conf.py + +python: + install: + - method: pip + path: . + requirements: docs/requirements.txt diff --git a/README.md b/README.md index 9d6bf7d..b0d0009 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,14 @@ [![Release](https://github.com/Quasars/pySNOM/actions/workflows/release.yml/badge.svg)](https://github.com/Quasars/pySNOM/actions/workflows/release.yml) # pySNOM Scanning Near-field Optical Microscopy (SNOM) analysis tools + +## Documentation + +The documentation uses Sphinx, MyST Markdown, and NumPy-style docstrings: + +```console +python -m pip install -r docs/requirements.txt +python -m sphinx -b html docs docs/_build/html +``` + +The generated site is available at `docs/_build/html/index.html`. diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..73a28c7 --- /dev/null +++ b/docs/Makefile @@ -0,0 +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 = . +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/_static/api-reference.svg b/docs/_static/api-reference.svg new file mode 100644 index 0000000..04ca4c1 --- /dev/null +++ b/docs/_static/api-reference.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/docs/_static/contribute.svg b/docs/_static/contribute.svg new file mode 100644 index 0000000..85600e9 --- /dev/null +++ b/docs/_static/contribute.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 0000000..2bc626c --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,80 @@ +.landing-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; + margin: 2rem 0 3rem; +} + +.bd-page-width { + max-width: 100rem !important; +} + +.landing-card { + display: block; + border: 1px solid var(--pst-color-border); + border-radius: 0.35rem; + padding: 1.25rem 1.4rem; + color: var(--pst-color-text-base); + background: var(--pst-color-background); + transition: border-color 150ms ease, box-shadow 150ms ease, + transform 150ms ease; +} + +.landing-card:hover { + border-color: var(--pst-color-primary); + box-shadow: 0 0.35rem 1rem rgb(0 0 0 / 10%); + color: var(--pst-color-text-base); + text-decoration: none; + transform: translateY(-2px); +} + +.landing-card__logo { + display: block; + width: 4.5rem; + height: 4.5rem; + margin-bottom: 1rem; + object-fit: contain; +} + +.landing-card h2 { + margin: 0.35rem 0 0.5rem; + color: var(--pst-color-primary); + font-size: 1.25rem; +} + +.landing-card p { + margin: 0; +} + +.landing-card__label { + color: var(--pst-color-text-muted); + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +html[data-theme="light"] .highlight-text { + margin: 1rem 0 1.5rem; + border: none; + border-left: 0.2rem solid #c8c8c8; + border-radius: 0.35rem; + background: #f5f5f5; +} + +html[data-theme="light"] .highlight-text pre { + margin: 0; + padding: 1rem 1.2rem; + color: var(--pst-color-text-base); + background: #f5f5f5; + font-family: var(--pst-font-family-monospace); + font-size: 0.9rem; + line-height: 1.55; + white-space: pre-wrap; +} + +@media (max-width: 700px) { + .landing-grid { + grid-template-columns: 1fr; + } +} diff --git a/docs/_static/getting-started.svg b/docs/_static/getting-started.svg new file mode 100644 index 0000000..5c51f0f --- /dev/null +++ b/docs/_static/getting-started.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/docs/_static/users-guide.svg b/docs/_static/users-guide.svg new file mode 100644 index 0000000..34a375a --- /dev/null +++ b/docs/_static/users-guide.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/docs/_templates/autosummary/attribute.rst b/docs/_templates/autosummary/attribute.rst new file mode 100644 index 0000000..f6c63a0 --- /dev/null +++ b/docs/_templates/autosummary/attribute.rst @@ -0,0 +1,13 @@ +:orphan: + +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +attribute + +.. auto{{ objtype }}:: {{ fullname | replace("pySNOM.", "pySNOM::") }} + +{# In the fullname (e.g. `numpy.ma.MaskedArray.methodname`), the module name +is ambiguous. Using a `::` separator (e.g. `numpy::ma.MaskedArray.methodname`) +specifies `numpy` as the module name. #} diff --git a/docs/_templates/autosummary/base.rst b/docs/_templates/autosummary/base.rst new file mode 100644 index 0000000..39d09c7 --- /dev/null +++ b/docs/_templates/autosummary/base.rst @@ -0,0 +1,15 @@ +:orphan: + +{% if objtype == 'property' %} +:orphan: +{% endif %} + +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +{% if objtype == 'property' %} +property +{% endif %} + +.. auto{{ objtype }}:: {{ fullname }} diff --git a/docs/_templates/autosummary/class.rst b/docs/_templates/autosummary/class.rst new file mode 100644 index 0000000..d6d500e --- /dev/null +++ b/docs/_templates/autosummary/class.rst @@ -0,0 +1,11 @@ +{% extends "!autosummary/class.rst" %} + +{% block methods %} {% if methods %} +{% for item in methods %} + .. automethod:: {{ item }} +{%- endfor %} +{% endif %} {% endblock %} + +{% block attributes %} {% if attributes %} + +{% endif %} {% endblock %} \ No newline at end of file diff --git a/docs/_templates/autosummary/member.rst b/docs/_templates/autosummary/member.rst new file mode 100644 index 0000000..c0fbd01 --- /dev/null +++ b/docs/_templates/autosummary/member.rst @@ -0,0 +1,9 @@ +:orphan: + +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +member + +.. auto{{ objtype }}:: {{ fullname | replace("pySNOM.", "pySNOM::") }} diff --git a/docs/_templates/autosummary/method.rst b/docs/_templates/autosummary/method.rst new file mode 100644 index 0000000..81aeb6c --- /dev/null +++ b/docs/_templates/autosummary/method.rst @@ -0,0 +1,9 @@ +:orphan: + +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +method + +.. auto{{ objtype }}:: {{ fullname | replace("pySNOM.", "pySNOM::") }} \ No newline at end of file diff --git a/docs/_templates/autosummary/minimal_module.rst b/docs/_templates/autosummary/minimal_module.rst new file mode 100644 index 0000000..fe71574 --- /dev/null +++ b/docs/_templates/autosummary/minimal_module.rst @@ -0,0 +1,8 @@ +{{ fullname | escape | underline}} + +.. automodule:: {{ fullname }} + + {% block docstring %} + {% endblock %} + + diff --git a/docs/_templates/autosummary/module.rst b/docs/_templates/autosummary/module.rst new file mode 100644 index 0000000..e977c02 --- /dev/null +++ b/docs/_templates/autosummary/module.rst @@ -0,0 +1,40 @@ +{% extends "!autosummary/module.rst" %} + +{# This file is almost the same as the default, but adds :toctree: to the autosummary directives. + The original can be found at `sphinx/ext/autosummary/templates/autosummary/module.rst`. #} + +{% block attributes %} +{% if attributes %} + .. rubric:: Module Attributes + + .. autosummary:: + :toctree: + {% for item in attributes %} + {{ item }} + {%- endfor %} +{% endif %} +{% endblock %} + +{% block functions %} +{% if functions %} + .. rubric:: Functions + + .. autosummary:: + :toctree: + {% for item in functions %} + {{ item }} + {%- endfor %} +{% endif %} +{% endblock %} + +{% block classes %} +{% if classes %} + .. rubric:: Classes + + .. autosummary:: + :toctree: + {% for item in classes %} + {{ item }} + {%- endfor %} +{% endif %} +{% endblock %} diff --git a/docs/api/defaults.rst b/docs/api/defaults.rst new file mode 100644 index 0000000..0461b5b --- /dev/null +++ b/docs/api/defaults.rst @@ -0,0 +1,14 @@ +Defaults (:mod:`pySNOM.defaults`) +================================= + +.. currentmodule:: pySNOM.defaults + +This module contains the default values for the pySNOM package. The :class:`pySNOM.defaults.Defaults` class is the main data structure for default values, +and it provides a consistent interface for accessing default values for different processing classes. + +.. autosummary:: + :toctree: generated + :template: autosummary/class.rst + + Defaults + diff --git a/docs/api/generated/pySNOM.defaults.Defaults.rst b/docs/api/generated/pySNOM.defaults.Defaults.rst new file mode 100644 index 0000000..10e1eb2 --- /dev/null +++ b/docs/api/generated/pySNOM.defaults.Defaults.rst @@ -0,0 +1,13 @@ +pySNOM.defaults.Defaults +======================== + +.. currentmodule:: pySNOM.defaults + +.. autoclass:: Defaults + + + + .. automethod:: __init__ + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.AlignImageStack.rst b/docs/api/generated/pySNOM.images.AlignImageStack.rst new file mode 100644 index 0000000..4e928e6 --- /dev/null +++ b/docs/api/generated/pySNOM.images.AlignImageStack.rst @@ -0,0 +1,15 @@ +pySNOM.images.AlignImageStack +============================= + +.. currentmodule:: pySNOM.images + +.. autoclass:: AlignImageStack + + + + .. automethod:: __init__ + .. automethod:: calculate + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.BackgroundPolyFit.rst b/docs/api/generated/pySNOM.images.BackgroundPolyFit.rst new file mode 100644 index 0000000..a03cef9 --- /dev/null +++ b/docs/api/generated/pySNOM.images.BackgroundPolyFit.rst @@ -0,0 +1,15 @@ +pySNOM.images.BackgroundPolyFit +=============================== + +.. currentmodule:: pySNOM.images + +.. autoclass:: BackgroundPolyFit + + + + .. automethod:: __init__ + .. automethod:: calculate + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.CalculateOpticalFlow.rst b/docs/api/generated/pySNOM.images.CalculateOpticalFlow.rst new file mode 100644 index 0000000..b2ddb63 --- /dev/null +++ b/docs/api/generated/pySNOM.images.CalculateOpticalFlow.rst @@ -0,0 +1,14 @@ +pySNOM.images.CalculateOpticalFlow +================================== + +.. currentmodule:: pySNOM.images + +.. autoclass:: CalculateOpticalFlow + + + + .. automethod:: __init__ + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.CalculateXCorrDrift.rst b/docs/api/generated/pySNOM.images.CalculateXCorrDrift.rst new file mode 100644 index 0000000..eaca3a3 --- /dev/null +++ b/docs/api/generated/pySNOM.images.CalculateXCorrDrift.rst @@ -0,0 +1,14 @@ +pySNOM.images.CalculateXCorrDrift +================================= + +.. currentmodule:: pySNOM.images + +.. autoclass:: CalculateXCorrDrift + + + + .. automethod:: __init__ + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.CorrectImageDrift.rst b/docs/api/generated/pySNOM.images.CorrectImageDrift.rst new file mode 100644 index 0000000..2cf1987 --- /dev/null +++ b/docs/api/generated/pySNOM.images.CorrectImageDrift.rst @@ -0,0 +1,14 @@ +pySNOM.images.CorrectImageDrift +=============================== + +.. currentmodule:: pySNOM.images + +.. autoclass:: CorrectImageDrift + + + + .. automethod:: __init__ + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.GwyImage.rst b/docs/api/generated/pySNOM.images.GwyImage.rst new file mode 100644 index 0000000..102fb14 --- /dev/null +++ b/docs/api/generated/pySNOM.images.GwyImage.rst @@ -0,0 +1,17 @@ +pySNOM.images.GwyImage +====================== + +.. currentmodule:: pySNOM.images + +.. autoclass:: GwyImage + + + + .. automethod:: __init__ + .. automethod:: extract_channel + .. automethod:: image_from_channel + + + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.Image.rst b/docs/api/generated/pySNOM.images.Image.rst new file mode 100644 index 0000000..7431961 --- /dev/null +++ b/docs/api/generated/pySNOM.images.Image.rst @@ -0,0 +1,17 @@ +pySNOM.images.Image +=================== + +.. currentmodule:: pySNOM.images + +.. autoclass:: Image + + + + .. automethod:: __init__ + .. automethod:: extract_channel + .. automethod:: image_from_channel + + + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.LaplaceFillIn.rst b/docs/api/generated/pySNOM.images.LaplaceFillIn.rst new file mode 100644 index 0000000..94c9053 --- /dev/null +++ b/docs/api/generated/pySNOM.images.LaplaceFillIn.rst @@ -0,0 +1,14 @@ +pySNOM.images.LaplaceFillIn +=========================== + +.. currentmodule:: pySNOM.images + +.. autoclass:: LaplaceFillIn + + + + .. automethod:: __init__ + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.LineLevel.rst b/docs/api/generated/pySNOM.images.LineLevel.rst new file mode 100644 index 0000000..c9b838c --- /dev/null +++ b/docs/api/generated/pySNOM.images.LineLevel.rst @@ -0,0 +1,16 @@ +pySNOM.images.LineLevel +======================= + +.. currentmodule:: pySNOM.images + +.. autoclass:: LineLevel + + + + .. automethod:: __init__ + .. automethod:: calculate + .. automethod:: correct + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.MaskedBackgroundPolyFit.rst b/docs/api/generated/pySNOM.images.MaskedBackgroundPolyFit.rst new file mode 100644 index 0000000..7c1ded6 --- /dev/null +++ b/docs/api/generated/pySNOM.images.MaskedBackgroundPolyFit.rst @@ -0,0 +1,16 @@ +pySNOM.images.MaskedBackgroundPolyFit +===================================== + +.. currentmodule:: pySNOM.images + +.. autoclass:: MaskedBackgroundPolyFit + + + + .. automethod:: __init__ + .. automethod:: calculate + .. automethod:: correct + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.MaskedTransformation.rst b/docs/api/generated/pySNOM.images.MaskedTransformation.rst new file mode 100644 index 0000000..459b168 --- /dev/null +++ b/docs/api/generated/pySNOM.images.MaskedTransformation.rst @@ -0,0 +1,16 @@ +pySNOM.images.MaskedTransformation +================================== + +.. currentmodule:: pySNOM.images + +.. autoclass:: MaskedTransformation + + + + .. automethod:: __init__ + .. automethod:: calculate + .. automethod:: correct + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.Measurement.rst b/docs/api/generated/pySNOM.images.Measurement.rst new file mode 100644 index 0000000..39ef012 --- /dev/null +++ b/docs/api/generated/pySNOM.images.Measurement.rst @@ -0,0 +1,17 @@ +pySNOM.images.Measurement +========================= + +.. currentmodule:: pySNOM.images + +.. autoclass:: Measurement + + + + .. automethod:: __init__ + .. automethod:: extract_channel + .. automethod:: image_from_channel + + + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.RemoveSpikes.rst b/docs/api/generated/pySNOM.images.RemoveSpikes.rst new file mode 100644 index 0000000..438ea76 --- /dev/null +++ b/docs/api/generated/pySNOM.images.RemoveSpikes.rst @@ -0,0 +1,14 @@ +pySNOM.images.RemoveSpikes +========================== + +.. currentmodule:: pySNOM.images + +.. autoclass:: RemoveSpikes + + + + .. automethod:: __init__ + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.RotatePhase.rst b/docs/api/generated/pySNOM.images.RotatePhase.rst new file mode 100644 index 0000000..77dbd2b --- /dev/null +++ b/docs/api/generated/pySNOM.images.RotatePhase.rst @@ -0,0 +1,14 @@ +pySNOM.images.RotatePhase +========================= + +.. currentmodule:: pySNOM.images + +.. autoclass:: RotatePhase + + + + .. automethod:: __init__ + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.ScarRemoval.rst b/docs/api/generated/pySNOM.images.ScarRemoval.rst new file mode 100644 index 0000000..d9d9111 --- /dev/null +++ b/docs/api/generated/pySNOM.images.ScarRemoval.rst @@ -0,0 +1,14 @@ +pySNOM.images.ScarRemoval +========================= + +.. currentmodule:: pySNOM.images + +.. autoclass:: ScarRemoval + + + + .. automethod:: __init__ + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.SelfReference.rst b/docs/api/generated/pySNOM.images.SelfReference.rst new file mode 100644 index 0000000..31d0596 --- /dev/null +++ b/docs/api/generated/pySNOM.images.SelfReference.rst @@ -0,0 +1,14 @@ +pySNOM.images.SelfReference +=========================== + +.. currentmodule:: pySNOM.images + +.. autoclass:: SelfReference + + + + .. automethod:: __init__ + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.SimpleNormalize.rst b/docs/api/generated/pySNOM.images.SimpleNormalize.rst new file mode 100644 index 0000000..55881b9 --- /dev/null +++ b/docs/api/generated/pySNOM.images.SimpleNormalize.rst @@ -0,0 +1,16 @@ +pySNOM.images.SimpleNormalize +============================= + +.. currentmodule:: pySNOM.images + +.. autoclass:: SimpleNormalize + + + + .. automethod:: __init__ + .. automethod:: calculate + .. automethod:: correct + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.Transformation.rst b/docs/api/generated/pySNOM.images.Transformation.rst new file mode 100644 index 0000000..d8c6cbc --- /dev/null +++ b/docs/api/generated/pySNOM.images.Transformation.rst @@ -0,0 +1,14 @@ +pySNOM.images.Transformation +============================ + +.. currentmodule:: pySNOM.images + +.. autoclass:: Transformation + + + + .. automethod:: __init__ + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.ValueFillIn.rst b/docs/api/generated/pySNOM.images.ValueFillIn.rst new file mode 100644 index 0000000..db26309 --- /dev/null +++ b/docs/api/generated/pySNOM.images.ValueFillIn.rst @@ -0,0 +1,14 @@ +pySNOM.images.ValueFillIn +========================= + +.. currentmodule:: pySNOM.images + +.. autoclass:: ValueFillIn + + + + .. automethod:: __init__ + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.WrapImage.rst b/docs/api/generated/pySNOM.images.WrapImage.rst new file mode 100644 index 0000000..a70fc55 --- /dev/null +++ b/docs/api/generated/pySNOM.images.WrapImage.rst @@ -0,0 +1,14 @@ +pySNOM.images.WrapImage +======================= + +.. currentmodule:: pySNOM.images + +.. autoclass:: WrapImage + + + + .. automethod:: __init__ + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.create_nparray_stack.rst b/docs/api/generated/pySNOM.images.create_nparray_stack.rst new file mode 100644 index 0000000..4e9b639 --- /dev/null +++ b/docs/api/generated/pySNOM.images.create_nparray_stack.rst @@ -0,0 +1,10 @@ +:orphan: + +pySNOM.images.create\_nparray\_stack +==================================== + +.. currentmodule:: pySNOM.images + +member + +.. autofunction:: pySNOM::images.create_nparray_stack \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.cut_cross_section.rst b/docs/api/generated/pySNOM.images.cut_cross_section.rst new file mode 100644 index 0000000..57ea3af --- /dev/null +++ b/docs/api/generated/pySNOM.images.cut_cross_section.rst @@ -0,0 +1,10 @@ +:orphan: + +pySNOM.images.cut\_cross\_section +================================= + +.. currentmodule:: pySNOM.images + +member + +.. autofunction:: pySNOM::images.cut_cross_section \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.dict_from_imagestack.rst b/docs/api/generated/pySNOM.images.dict_from_imagestack.rst new file mode 100644 index 0000000..d891825 --- /dev/null +++ b/docs/api/generated/pySNOM.images.dict_from_imagestack.rst @@ -0,0 +1,10 @@ +:orphan: + +pySNOM.images.dict\_from\_imagestack +==================================== + +.. currentmodule:: pySNOM.images + +member + +.. autofunction:: pySNOM::images.dict_from_imagestack \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.flatten_stack.rst b/docs/api/generated/pySNOM.images.flatten_stack.rst new file mode 100644 index 0000000..3da5326 --- /dev/null +++ b/docs/api/generated/pySNOM.images.flatten_stack.rst @@ -0,0 +1,10 @@ +:orphan: + +pySNOM.images.flatten\_stack +============================ + +.. currentmodule:: pySNOM.images + +member + +.. autofunction:: pySNOM::images.flatten_stack \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.mask_from_booleans.rst b/docs/api/generated/pySNOM.images.mask_from_booleans.rst new file mode 100644 index 0000000..10ac02d --- /dev/null +++ b/docs/api/generated/pySNOM.images.mask_from_booleans.rst @@ -0,0 +1,10 @@ +:orphan: + +pySNOM.images.mask\_from\_booleans +================================== + +.. currentmodule:: pySNOM.images + +member + +.. autofunction:: pySNOM::images.mask_from_booleans \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.mask_from_datacondition.rst b/docs/api/generated/pySNOM.images.mask_from_datacondition.rst new file mode 100644 index 0000000..da855e1 --- /dev/null +++ b/docs/api/generated/pySNOM.images.mask_from_datacondition.rst @@ -0,0 +1,10 @@ +:orphan: + +pySNOM.images.mask\_from\_datacondition +======================================= + +.. currentmodule:: pySNOM.images + +member + +.. autofunction:: pySNOM::images.mask_from_datacondition \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.shift_fill.rst b/docs/api/generated/pySNOM.images.shift_fill.rst new file mode 100644 index 0000000..8771e98 --- /dev/null +++ b/docs/api/generated/pySNOM.images.shift_fill.rst @@ -0,0 +1,10 @@ +:orphan: + +pySNOM.images.shift\_fill +========================= + +.. currentmodule:: pySNOM.images + +member + +.. autofunction:: pySNOM::images.shift_fill \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.sort_image_stack.rst b/docs/api/generated/pySNOM.images.sort_image_stack.rst new file mode 100644 index 0000000..a736d56 --- /dev/null +++ b/docs/api/generated/pySNOM.images.sort_image_stack.rst @@ -0,0 +1,10 @@ +:orphan: + +pySNOM.images.sort\_image\_stack +================================ + +.. currentmodule:: pySNOM.images + +member + +.. autofunction:: pySNOM::images.sort_image_stack \ No newline at end of file diff --git a/docs/api/generated/pySNOM.images.type_from_channelname.rst b/docs/api/generated/pySNOM.images.type_from_channelname.rst new file mode 100644 index 0000000..565cc4a --- /dev/null +++ b/docs/api/generated/pySNOM.images.type_from_channelname.rst @@ -0,0 +1,10 @@ +:orphan: + +pySNOM.images.type\_from\_channelname +===================================== + +.. currentmodule:: pySNOM.images + +member + +.. autofunction:: pySNOM::images.type_from_channelname \ No newline at end of file diff --git a/docs/api/generated/pySNOM.interferograms.InterpolateInterferogram.rst b/docs/api/generated/pySNOM.interferograms.InterpolateInterferogram.rst new file mode 100644 index 0000000..1ad8833 --- /dev/null +++ b/docs/api/generated/pySNOM.interferograms.InterpolateInterferogram.rst @@ -0,0 +1,14 @@ +pySNOM.interferograms.InterpolateInterferogram +============================================== + +.. currentmodule:: pySNOM.interferograms + +.. autoclass:: InterpolateInterferogram + + + + .. automethod:: __init__ + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.interferograms.NeaInterferogram.rst b/docs/api/generated/pySNOM.interferograms.NeaInterferogram.rst new file mode 100644 index 0000000..22d2d8b --- /dev/null +++ b/docs/api/generated/pySNOM.interferograms.NeaInterferogram.rst @@ -0,0 +1,16 @@ +pySNOM.interferograms.NeaInterferogram +====================================== + +.. currentmodule:: pySNOM.interferograms + +.. autoclass:: NeaInterferogram + + + + .. automethod:: __init__ + .. automethod:: add_channel + + + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.interferograms.ProcessAllPoints.rst b/docs/api/generated/pySNOM.interferograms.ProcessAllPoints.rst new file mode 100644 index 0000000..b2a04f7 --- /dev/null +++ b/docs/api/generated/pySNOM.interferograms.ProcessAllPoints.rst @@ -0,0 +1,14 @@ +pySNOM.interferograms.ProcessAllPoints +====================================== + +.. currentmodule:: pySNOM.interferograms + +.. autoclass:: ProcessAllPoints + + + + .. automethod:: __init__ + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.interferograms.ProcessInterferogram.rst b/docs/api/generated/pySNOM.interferograms.ProcessInterferogram.rst new file mode 100644 index 0000000..c89caee --- /dev/null +++ b/docs/api/generated/pySNOM.interferograms.ProcessInterferogram.rst @@ -0,0 +1,14 @@ +pySNOM.interferograms.ProcessInterferogram +========================================== + +.. currentmodule:: pySNOM.interferograms + +.. autoclass:: ProcessInterferogram + + + + .. automethod:: __init__ + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.interferograms.ProcessMultiChannels.rst b/docs/api/generated/pySNOM.interferograms.ProcessMultiChannels.rst new file mode 100644 index 0000000..295f28d --- /dev/null +++ b/docs/api/generated/pySNOM.interferograms.ProcessMultiChannels.rst @@ -0,0 +1,14 @@ +pySNOM.interferograms.ProcessMultiChannels +========================================== + +.. currentmodule:: pySNOM.interferograms + +.. autoclass:: ProcessMultiChannels + + + + .. automethod:: __init__ + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.interferograms.ProcessSingleChannel.rst b/docs/api/generated/pySNOM.interferograms.ProcessSingleChannel.rst new file mode 100644 index 0000000..05e19d6 --- /dev/null +++ b/docs/api/generated/pySNOM.interferograms.ProcessSingleChannel.rst @@ -0,0 +1,14 @@ +pySNOM.interferograms.ProcessSingleChannel +========================================== + +.. currentmodule:: pySNOM.interferograms + +.. autoclass:: ProcessSingleChannel + + + + .. automethod:: __init__ + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.interferograms.Tools.rst b/docs/api/generated/pySNOM.interferograms.Tools.rst new file mode 100644 index 0000000..9773e53 --- /dev/null +++ b/docs/api/generated/pySNOM.interferograms.Tools.rst @@ -0,0 +1,17 @@ +pySNOM.interferograms.Tools +=========================== + +.. currentmodule:: pySNOM.interferograms + +.. autoclass:: Tools + + + + .. automethod:: __init__ + .. automethod:: analyse_steps + .. automethod:: asymmetric_window + .. automethod:: reshape_ifg_data + .. automethod:: reshape_linescan_interferogram + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.readers.GsfReader.rst b/docs/api/generated/pySNOM.readers.GsfReader.rst new file mode 100644 index 0000000..28b5706 --- /dev/null +++ b/docs/api/generated/pySNOM.readers.GsfReader.rst @@ -0,0 +1,14 @@ +pySNOM.readers.GsfReader +======================== + +.. currentmodule:: pySNOM.readers + +.. autoclass:: GsfReader + + + + .. automethod:: __init__ + .. automethod:: read + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.readers.GwyReader.rst b/docs/api/generated/pySNOM.readers.GwyReader.rst new file mode 100644 index 0000000..e75c625 --- /dev/null +++ b/docs/api/generated/pySNOM.readers.GwyReader.rst @@ -0,0 +1,14 @@ +pySNOM.readers.GwyReader +======================== + +.. currentmodule:: pySNOM.readers + +.. autoclass:: GwyReader + + + + .. automethod:: __init__ + .. automethod:: read + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.readers.ImageStackReader.rst b/docs/api/generated/pySNOM.readers.ImageStackReader.rst new file mode 100644 index 0000000..97bdeec --- /dev/null +++ b/docs/api/generated/pySNOM.readers.ImageStackReader.rst @@ -0,0 +1,14 @@ +pySNOM.readers.ImageStackReader +=============================== + +.. currentmodule:: pySNOM.readers + +.. autoclass:: ImageStackReader + + + + .. automethod:: __init__ + .. automethod:: read + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.readers.ImageStackXYZReader.rst b/docs/api/generated/pySNOM.readers.ImageStackXYZReader.rst new file mode 100644 index 0000000..5bc600d --- /dev/null +++ b/docs/api/generated/pySNOM.readers.ImageStackXYZReader.rst @@ -0,0 +1,14 @@ +pySNOM.readers.ImageStackXYZReader +================================== + +.. currentmodule:: pySNOM.readers + +.. autoclass:: ImageStackXYZReader + + + + .. automethod:: __init__ + .. automethod:: read + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.readers.NeaFileLegacyReader.rst b/docs/api/generated/pySNOM.readers.NeaFileLegacyReader.rst new file mode 100644 index 0000000..3189c08 --- /dev/null +++ b/docs/api/generated/pySNOM.readers.NeaFileLegacyReader.rst @@ -0,0 +1,14 @@ +pySNOM.readers.NeaFileLegacyReader +================================== + +.. currentmodule:: pySNOM.readers + +.. autoclass:: NeaFileLegacyReader + + + + .. automethod:: __init__ + .. automethod:: read + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.readers.NeaHeaderReader.rst b/docs/api/generated/pySNOM.readers.NeaHeaderReader.rst new file mode 100644 index 0000000..bf5be5e --- /dev/null +++ b/docs/api/generated/pySNOM.readers.NeaHeaderReader.rst @@ -0,0 +1,15 @@ +pySNOM.readers.NeaHeaderReader +============================== + +.. currentmodule:: pySNOM.readers + +.. autoclass:: NeaHeaderReader + + + + .. automethod:: __init__ + .. automethod:: parseline + .. automethod:: read + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.readers.NeaInfoReader.rst b/docs/api/generated/pySNOM.readers.NeaInfoReader.rst new file mode 100644 index 0000000..47b78ec --- /dev/null +++ b/docs/api/generated/pySNOM.readers.NeaInfoReader.rst @@ -0,0 +1,15 @@ +pySNOM.readers.NeaInfoReader +============================ + +.. currentmodule:: pySNOM.readers + +.. autoclass:: NeaInfoReader + + + + .. automethod:: __init__ + .. automethod:: parseline + .. automethod:: read + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.readers.NeaSpectralReader.rst b/docs/api/generated/pySNOM.readers.NeaSpectralReader.rst new file mode 100644 index 0000000..8e840f2 --- /dev/null +++ b/docs/api/generated/pySNOM.readers.NeaSpectralReader.rst @@ -0,0 +1,14 @@ +pySNOM.readers.NeaSpectralReader +================================ + +.. currentmodule:: pySNOM.readers + +.. autoclass:: NeaSpectralReader + + + + .. automethod:: __init__ + .. automethod:: read + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.readers.Reader.rst b/docs/api/generated/pySNOM.readers.Reader.rst new file mode 100644 index 0000000..4392cb9 --- /dev/null +++ b/docs/api/generated/pySNOM.readers.Reader.rst @@ -0,0 +1,13 @@ +pySNOM.readers.Reader +===================== + +.. currentmodule:: pySNOM.readers + +.. autoclass:: Reader + + + + .. automethod:: __init__ + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.readers.get_filenames.rst b/docs/api/generated/pySNOM.readers.get_filenames.rst new file mode 100644 index 0000000..2ba40f8 --- /dev/null +++ b/docs/api/generated/pySNOM.readers.get_filenames.rst @@ -0,0 +1,10 @@ +:orphan: + +pySNOM.readers.get\_filenames +============================= + +.. currentmodule:: pySNOM.readers + +member + +.. autofunction:: pySNOM::readers.get_filenames \ No newline at end of file diff --git a/docs/api/generated/pySNOM.readers.get_wl_from_filename.rst b/docs/api/generated/pySNOM.readers.get_wl_from_filename.rst new file mode 100644 index 0000000..6b9e178 --- /dev/null +++ b/docs/api/generated/pySNOM.readers.get_wl_from_filename.rst @@ -0,0 +1,10 @@ +:orphan: + +pySNOM.readers.get\_wl\_from\_filename +====================================== + +.. currentmodule:: pySNOM.readers + +member + +.. autofunction:: pySNOM::readers.get_wl_from_filename \ No newline at end of file diff --git a/docs/api/generated/pySNOM.readers.get_wl_from_infofile.rst b/docs/api/generated/pySNOM.readers.get_wl_from_infofile.rst new file mode 100644 index 0000000..762528f --- /dev/null +++ b/docs/api/generated/pySNOM.readers.get_wl_from_infofile.rst @@ -0,0 +1,10 @@ +:orphan: + +pySNOM.readers.get\_wl\_from\_infofile +====================================== + +.. currentmodule:: pySNOM.readers + +member + +.. autofunction:: pySNOM::readers.get_wl_from_infofile \ No newline at end of file diff --git a/docs/api/generated/pySNOM.readers.recreate_infofile_name_from_path.rst b/docs/api/generated/pySNOM.readers.recreate_infofile_name_from_path.rst new file mode 100644 index 0000000..debf045 --- /dev/null +++ b/docs/api/generated/pySNOM.readers.recreate_infofile_name_from_path.rst @@ -0,0 +1,10 @@ +:orphan: + +pySNOM.readers.recreate\_infofile\_name\_from\_path +=================================================== + +.. currentmodule:: pySNOM.readers + +member + +.. autofunction:: pySNOM::readers.recreate_infofile_name_from_path \ No newline at end of file diff --git a/docs/api/generated/pySNOM.spectra.ConstantNormalize.rst b/docs/api/generated/pySNOM.spectra.ConstantNormalize.rst new file mode 100644 index 0000000..453d8ba --- /dev/null +++ b/docs/api/generated/pySNOM.spectra.ConstantNormalize.rst @@ -0,0 +1,14 @@ +pySNOM.spectra.ConstantNormalize +================================ + +.. currentmodule:: pySNOM.spectra + +.. autoclass:: ConstantNormalize + + + + .. automethod:: __init__ + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.spectra.Cut.rst b/docs/api/generated/pySNOM.spectra.Cut.rst new file mode 100644 index 0000000..36347d9 --- /dev/null +++ b/docs/api/generated/pySNOM.spectra.Cut.rst @@ -0,0 +1,14 @@ +pySNOM.spectra.Cut +================== + +.. currentmodule:: pySNOM.spectra + +.. autoclass:: Cut + + + + .. automethod:: __init__ + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.spectra.LinearNormalize.rst b/docs/api/generated/pySNOM.spectra.LinearNormalize.rst new file mode 100644 index 0000000..b48ea3c --- /dev/null +++ b/docs/api/generated/pySNOM.spectra.LinearNormalize.rst @@ -0,0 +1,14 @@ +pySNOM.spectra.LinearNormalize +============================== + +.. currentmodule:: pySNOM.spectra + +.. autoclass:: LinearNormalize + + + + .. automethod:: __init__ + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.spectra.NeaSpectrum.rst b/docs/api/generated/pySNOM.spectra.NeaSpectrum.rst new file mode 100644 index 0000000..74c9b8e --- /dev/null +++ b/docs/api/generated/pySNOM.spectra.NeaSpectrum.rst @@ -0,0 +1,16 @@ +pySNOM.spectra.NeaSpectrum +========================== + +.. currentmodule:: pySNOM.spectra + +.. autoclass:: NeaSpectrum + + + + .. automethod:: __init__ + .. automethod:: add_channel + + + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.spectra.NormalizeSpectrum.rst b/docs/api/generated/pySNOM.spectra.NormalizeSpectrum.rst new file mode 100644 index 0000000..48650db --- /dev/null +++ b/docs/api/generated/pySNOM.spectra.NormalizeSpectrum.rst @@ -0,0 +1,14 @@ +pySNOM.spectra.NormalizeSpectrum +================================ + +.. currentmodule:: pySNOM.spectra + +.. autoclass:: NormalizeSpectrum + + + + .. automethod:: __init__ + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.spectra.RotatePhase.rst b/docs/api/generated/pySNOM.spectra.RotatePhase.rst new file mode 100644 index 0000000..9f57914 --- /dev/null +++ b/docs/api/generated/pySNOM.spectra.RotatePhase.rst @@ -0,0 +1,14 @@ +pySNOM.spectra.RotatePhase +========================== + +.. currentmodule:: pySNOM.spectra + +.. autoclass:: RotatePhase + + + + .. automethod:: __init__ + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.spectra.Scale.rst b/docs/api/generated/pySNOM.spectra.Scale.rst new file mode 100644 index 0000000..4f7dcab --- /dev/null +++ b/docs/api/generated/pySNOM.spectra.Scale.rst @@ -0,0 +1,14 @@ +pySNOM.spectra.Scale +==================== + +.. currentmodule:: pySNOM.spectra + +.. autoclass:: Scale + + + + .. automethod:: __init__ + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.spectra.ShiftPhaseToZero.rst b/docs/api/generated/pySNOM.spectra.ShiftPhaseToZero.rst new file mode 100644 index 0000000..859a766 --- /dev/null +++ b/docs/api/generated/pySNOM.spectra.ShiftPhaseToZero.rst @@ -0,0 +1,14 @@ +pySNOM.spectra.ShiftPhaseToZero +=============================== + +.. currentmodule:: pySNOM.spectra + +.. autoclass:: ShiftPhaseToZero + + + + .. automethod:: __init__ + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.spectra.SingleChannelSpectrum.rst b/docs/api/generated/pySNOM.spectra.SingleChannelSpectrum.rst new file mode 100644 index 0000000..95bfcf1 --- /dev/null +++ b/docs/api/generated/pySNOM.spectra.SingleChannelSpectrum.rst @@ -0,0 +1,16 @@ +pySNOM.spectra.SingleChannelSpectrum +==================================== + +.. currentmodule:: pySNOM.spectra + +.. autoclass:: SingleChannelSpectrum + + + + .. automethod:: __init__ + .. automethod:: add_channel + + + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.spectra.Tools.rst b/docs/api/generated/pySNOM.spectra.Tools.rst new file mode 100644 index 0000000..19ec843 --- /dev/null +++ b/docs/api/generated/pySNOM.spectra.Tools.rst @@ -0,0 +1,14 @@ +pySNOM.spectra.Tools +==================== + +.. currentmodule:: pySNOM.spectra + +.. autoclass:: Tools + + + + .. automethod:: __init__ + .. automethod:: reshape_spectrum_data + + + \ No newline at end of file diff --git a/docs/api/generated/pySNOM.spectra.Transformation.rst b/docs/api/generated/pySNOM.spectra.Transformation.rst new file mode 100644 index 0000000..e44fb29 --- /dev/null +++ b/docs/api/generated/pySNOM.spectra.Transformation.rst @@ -0,0 +1,14 @@ +pySNOM.spectra.Transformation +============================= + +.. currentmodule:: pySNOM.spectra + +.. autoclass:: Transformation + + + + .. automethod:: __init__ + .. automethod:: transform + + + \ No newline at end of file diff --git a/docs/api/images.rst b/docs/api/images.rst new file mode 100644 index 0000000..81724f1 --- /dev/null +++ b/docs/api/images.rst @@ -0,0 +1,69 @@ +Images processing (:mod:`pySNOM.images`) +======================================== + +.. currentmodule:: pySNOM.images + +This module contains the main image processing classes and functions. The :class:`pySNOM.Image` class is the main data structure for images, +and the :class:`pySNOM.Transformation` class is the base class for all image processing transformations. The :class:`pySNOM.Measurement` +class is the base class for all measurement data structures. + +Data classes +^^^^^^^^^^^^ +The following classes are the main data structures for images and measurements. They help to manage the data and provide a consistent +interface for processing and analysis but it is not neccessary to use them directly. + +.. autosummary:: + :toctree: generated + :template: autosummary/class.rst + + Measurement + Image + GwyImage + +Transformer classes +^^^^^^^^^^^^^^^^^^^ +Transformer classes are used to apply transformations to images. They are derived from the :class:`pySNOM.Transformation` base class and implement +the :meth:`pySNOM.Transformation.transform` method to perform the image corrections. Some classes implement a :meth:`calculate` method to extract the correction values. +The extracted values can be used to apply the processing to another image by feeding the values to the :meth:`correct` method of the same class. +For these tranformers the `transform` method is a combination of `calculate` and `correct`. + +.. autosummary:: + :toctree: generated + :template: autosummary/class.rst + + Transformation + MaskedTransformation + LineLevel + RotatePhase + SelfReference + SimpleNormalize + BackgroundPolyFit + MaskedBackgroundPolyFit + LaplaceFillIn + ValueFillIn + RemoveSpikes + ScarRemoval + CalculateOpticalFlow + WrapImage + CalculateXCorrDrift + CorrectImageDrift + AlignImageStack + +Other functions +^^^^^^^^^^^^^^^ +This module also contains some utility functions for image processing. +They are not part of any class and can be used directly. + +.. autosummary:: + :toctree: generated + :template: autosummary/member.rst + + type_from_channelname + mask_from_booleans + mask_from_datacondition + cut_cross_section + shift_fill + sort_image_stack + create_nparray_stack + dict_from_imagestack + flatten_stack diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 0000000..05a9c7d --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,15 @@ +# API reference + +The following pages are generated from the Python modules and their +NumPy-style docstrings. + +```{toctree} +:maxdepth: 2 + +package +defaults +readers +images +spectra +interferograms +``` diff --git a/docs/api/interferograms.rst b/docs/api/interferograms.rst new file mode 100644 index 0000000..c15705b --- /dev/null +++ b/docs/api/interferograms.rst @@ -0,0 +1,58 @@ +Interferogram processing (:mod:`pySNOM.interferograms`) +======================================================= + +.. currentmodule:: pySNOM.interferograms + +This module contains the main interferogram processing classes and functions. +The :class:`pySNOM.interferograms.NeaInterferogram` class is the main data structure for interferograms, +and the :class:`pySNOM.interferograms.Transformation` class is the base class for all interferogram processing transformations. +Most of the transformations organized to be high level, simple to use, on the :class:`pySNOM.interferograms.NeaInterferogram` class, +but the most basic operations can be applied to arraylike data structures as well. + +Data classes +^^^^^^^^^^^^ +The following classes are the main data structures for images and measurements. They help to manage the data and provide a consistent +interface for processing and analysis but it is not neccessary to use them directly. + +.. autosummary:: + :toctree: generated + :template: autosummary/class.rst + + NeaInterferogram + +Transformer classes +^^^^^^^^^^^^^^^^^^^ +Transformer classes are used to apply transformations to interferograms. +They are derived from the :class:`pySNOM.interferograms.Transformation` base class and implement +the :meth:`pySNOM.interferograms.Transformation.transform` method to perform the interferogram processing. + +Simple transformers +"""""""""""""""""""""""""""""""""""""" +.. autosummary:: + :toctree: generated + :template: autosummary/class.rst + + ProcessInterferogram + InterpolateInterferogram + +:class:`NeaInterferogram` transformers +"""""""""""""""""""""""""""""""""""""" +.. autosummary:: + :toctree: generated + :template: autosummary/class.rst + + ProcessSingleChannel + ProcessMultiChannels + ProcessAllPoints + +Additional tools +"""""""""""""""""""""""""""""""""""""" +In addition to the main data structures and transformers, +this module also contains some utility classes and functions +that can be used for interferogram processing pipelines. They are wrapped in the following classes. + +.. autosummary:: + :toctree: generated + :template: autosummary/class.rst + + Tools diff --git a/docs/api/package.rst b/docs/api/package.rst new file mode 100644 index 0000000..8f89c91 --- /dev/null +++ b/docs/api/package.rst @@ -0,0 +1,11 @@ +Package API +=========== + +.. py:module:: pySNOM + +The top-level package exports the principal pySNOM data and processing classes: + +* :class:`pySNOM.Image` +* :class:`pySNOM.NeaInterferogram` +* :class:`pySNOM.NeaSpectrum` +* :class:`pySNOM.Reader` diff --git a/docs/api/readers.rst b/docs/api/readers.rst new file mode 100644 index 0000000..357a2b9 --- /dev/null +++ b/docs/api/readers.rst @@ -0,0 +1,62 @@ +File readers (:mod:`pySNOM.readers`) +==================================== + +.. currentmodule:: pySNOM.readers + +This module contains the main file readers for pySNOM. The :class:`pySNOM.readers.Reader` class is the base class for all readers, +and it provides a consistent interface for reading data from different file formats. The following classes are derived from the +:class:`pySNOM.readers.Reader` base class and implement the :meth:`pySNOM.readers.Reader.read` method to read data from specific file formats. + +Base class +^^^^^^^^^^ + +.. autosummary:: + :toctree: generated + :template: autosummary/class.rst + + Reader + +Image readers +^^^^^^^^^^^^^ + +.. autosummary:: + :toctree: generated + :template: autosummary/class.rst + + GwyReader + GsfReader + ImageStackReader + ImageStackXYZReader + +Spectrum readers +^^^^^^^^^^^^^^^^ + +.. autosummary:: + :toctree: generated + :template: autosummary/class.rst + + NeaSpectralReader + NeaFileLegacyReader + +Info file readers and helpers +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autosummary:: + :toctree: generated + :template: autosummary/class.rst + + NeaHeaderReader + NeaInfoReader + +Utility functions +^^^^^^^^^^^^^^^^^ + +.. autosummary:: + :toctree: generated + :template: autosummary/member.rst + + get_wl_from_infofile + get_wl_from_filename + get_filenames + recreate_infofile_name_from_path + diff --git a/docs/api/spectra.rst b/docs/api/spectra.rst new file mode 100644 index 0000000..39ccf9a --- /dev/null +++ b/docs/api/spectra.rst @@ -0,0 +1,50 @@ +Spectrum processing (:mod:`pySNOM.spectra`) +======================================================= + +.. currentmodule:: pySNOM.spectra + +This module contains the main spectrum processing classes and functions. +The :class:`pySNOM.spectra.NeaSpectrum` class is the main data structure for spectra, +and the :class:`pySNOM.spectra.Transformation` class is the base class for all spectrum processing transformations. + +Data classes +^^^^^^^^^^^^ +The following classes are the main data structures for images and measurements. They help to manage the data and provide a consistent +interface for processing and analysis but it is not neccessary to use them directly. + +.. autosummary:: + :toctree: generated + :template: autosummary/class.rst + + NeaSpectrum + SingleChannelSpectrum + +Transformer classes +^^^^^^^^^^^^^^^^^^^ +Transformer classes are used to apply transformations to spectral data. They are derived from the :class:`pySNOM.Transformation` base class and implement +the :meth:`pySNOM.Transformation.transform` method to perform the spectrum corrections. They usually accept arraylike data structures and return the processed data. + +.. autosummary:: + :toctree: generated + :template: autosummary/class.rst + + Transformation + Cut + Scale + LinearNormalize + ConstantNormalize + RotatePhase + ShiftPhaseToZero + NormalizeSpectrum + +Additional tools +^^^^^^^^^^^^^^^^ +In addition to the main data structures and transformers, +this module also contains some utility classes and functions +that can be used for spectrum processing pipelines. They are wrapped in the following classes. + +.. autosummary:: + :toctree: generated + :template: autosummary/class.rst + + Tools \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..e1f4cf9 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,59 @@ +from pathlib import Path +import sys + + +project = "pySNOM" +copyright = "2026, pySNOM contributors" +author = "pySNOM contributors" + +# Import the package directly from this checkout for autodoc. +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from pySNOM import __version__ + +version = release = __version__ + +extensions = [ + "myst_parser", + "sphinx.ext.autodoc", + 'numpydoc', + 'sphinx_copybutton', + 'sphinx_design', + 'sphinx.ext.doctest', + 'sphinx.ext.autosummary', + 'sphinx.ext.viewcode', + 'IPython.sphinxext.ipython_console_highlighting', + 'IPython.sphinxext.ipython_directive', +] + +templates_path = ["_templates"] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +html_theme = "pydata_sphinx_theme" +html_title = "pySNOM documentation" +html_static_path = ["_static"] +html_css_files = ["custom.css"] +html_theme_options = { + "github_url": "https://github.com/Quasars/pySNOM", + "navbar_align": "content", + "navbar_center": ["navbar-nav"], + "navbar_end": ["navbar-icon-links", "theme-switcher"], + "navbar_persistent": ["search-button"], + "show_nav_level": 0, + "collapse_navigation": True, + "show_toc_level": 2, + "navigation_depth": 4, +} +html_sidebars = { + "**": ["sidebar-nav-bs", "sidebar-ethical-ads"], +} + +autosummary_generate = True +autosummary_imported_members = False +numpydoc_class_members_toctree = False + +add_function_parentheses = True +numpydoc_use_plots = True + +plot_html_show_formats = False +plot_html_show_source_link = False + diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 0000000..412af2b --- /dev/null +++ b/docs/development.md @@ -0,0 +1,208 @@ +# Community + +Is something missing? Have you spotted a bug? Read on for details on how to +raise feature requests, or contribute to `pySNOM`. + +## Bugs, issues, features + +We are constantly adding new features and fixing existing bugs in the package. Hower if you spot issues or you have new ideas don't hasitate to create an issue on the +[pySNOM GitHub repository](https://github.com/Quasars/pySNOM/issues). + +## Join and contribute + +If you like coding and want to to maximize the impact of your own work, please join us to make pySNOM the most tremendous data processing package. +By joining the project you advance science and help other researchers. We believe that open-source project are the best way to build something great and useful. + +I hope you decided to join the development. If so, here is a little guidance how you can contribute to the code. + +### 1. Create a local copy of the repository + +- **Fork the repository.** + You will want a "fork" repository to base your changes. We follow the fork -> branch -> pull request -> merge workflow common on GitHub. See https://help.github.com/articles/fork-a-repo/ for more details. + Go to the [pySNOM repository](https://github.com/Quasars/pySNOM) and click the "fork" + button to create your own copy of the project. +- **Clone the repository.** Open a terminal in the directory where you'd like + the project to be stored, then clone the project to your local computer: + + ```console + git clone https://github.com/your-username/pySNOM.git + ``` + +- **Link your local copy to the main repository.** Change the directory to + your newly created local repository: + + ```console + cd pySNOM + ``` + + Now, add the upstream repository: + + ```console + git remote add upstream https://github.com/Quasars/pySNOM.git + ``` + + Then, `git remote -v` will show two remote repositories named: + + - `upstream`, which refers to the `pySNOM` repository + - `origin`, which refers to your personal fork + +- **Update your repository.** Make sure your local repository is up-to-date, + by pulling the latest changes from upstream: + + ```console + git checkout master + git pull upstream master + ``` + +### 2. Create a Python environment for development + +Once you have a git checkout of the code, you will want some kind of virtual environment to keep your development separate from your regular pySNOM install (and other Python work/programs you may have). The follow describes how to do this using the Anaconda conda environment system, but can also apply to virtualenvs. + +- **Create a fresh environment.** +Run "Anaconda Prompt" or similar and: + + conda config --add channels conda-forge + conda create --name="pysnom-dev" + conda activate pysnom-dev + +- **Install pySNOM.** In your clean environment, navigate to the pySNOM + repository and install pySNOM and its dependencies in editable mode: + + +Navigate to your pySNOM directory, then install in development mode: + + pip install -e . + +It is highly recommended to set up `pre-commit` hooks to automatically check your code quality before each commit: + + pre-commit install + +- **Set up testing.** `pySNOM` uses the standard library `unittest` framework + for its test suite. Your contributions won't be accepted unless all the + tests pass, so check the tests run on your own computer before you submit + your changes. If all went well, you should be able to run the tests: + + python -m unittest orangecontrib.spectroscopy.tests + + This should run the tests in `pySNOM/tests`, which should all pass + successfully. + +- **Set up documentation tools.** All the features in `pySNOM` should be + documented, so if your edit adds a new feature, or changes how other users + will interact with the package, we ask that you also add changes to the + documentation to explain it. We use [`sphinx`](https://www.sphinx-doc.org/) + to build our documentation. Install the documentation dependencies: + + ```console + python -m pip install -r docs/requirements.txt + ``` + + You can then build the documentation with `sphinx-autobuild` to host it on + a local server and preview any changes you make: + + ```console + sphinx-autobuild docs docs/_build/html + ``` + + Or build a static copy with: + + ```console + cd docs + make html + ``` + +### 3. Develop your contribution + +- **Create a git branch for the feature you want to work on.** + Use a descriptive name such as `new-feature`. Try to keep your main branch up-to-date with the upstream repository and base the new branch on top of that: + + ```console + git checkout -b new-feature + ``` + +- **Commit changes locally as you progress.** Use `git add` and + `git commit` with descriptive commit messages. +- **Create tests first.** All features in `pySNOM` should be tested to check + they work. Create at least one test that will only pass once the new feature works + correctly. Add your new test to the existing test suite in + `pySNOM/tests`, then run the test suite: + + ```console + python -m unittest pySNOM.tests + ``` +- **Add your changes to the package.** Make sure that you document your + changes by adding or editing docstrings, following the style guide below. +- **Make your final checks.** If your changes are successful, when you run + the test suite there should be no errors. Also check that all your code is + formatted correctly (this should be done automatically when you commit + your changes if you installed `pre-commit` as above), and that any changes + are documented. Then you should be ready to submit to the main repository. + +### 4. Submit your contribution + +- **Push your changes back to your fork on GitHub**: + + ```console + git push origin new-feature + ``` + +- **Make a pull request.** Go to GitHub. The new branch will show up with a + green Pull Request button. Make sure the title and message are clear, + concise, and self-explanatory. Then click the button to submit it. + +### 5. Review process + +- **Automatic tests.** When you make a pull request, GitHub will run tests to + check the suite of tests on different operating systems using several + versions of Python. The tests must pass for us to merge your changes, so we + recommend checking on your own computer before submitting a pull request. + +- **The pySNOM team will review your pull request.** A pull request must be + approved by at least one pySNOM team member before merging. + +## Communication + +It's a good idea to let the team know what you're working on before embarking, +especially for a large project. We prefer to discuss this in GitHub issues, so please +file one describing your contribution before getting too far in. + +For large projects in particular, consider working in a public Pull Request marked +`[WIP]` (work-in-progress) and ask for feedback along the way. + +## Docstring style + +Sphinx uses the Napoleon extension to render NumPy-style Python docstrings as +structured documentation. New public classes and methods should follow this +pattern: + +```python +def scale(values, factor=1.0): + """Scale an array by a constant factor. + + Parameters + ---------- + values : numpy.ndarray + Values to scale. + factor : float, optional + Multiplicative factor, by default 1.0. + + Returns + ------- + numpy.ndarray + Scaled values. + + Raises + ------ + ValueError + If ``factor`` is not finite. + + Examples + -------- + >>> scale(np.array([1.0, 2.0]), factor=2.0) + array([2., 4.]) + """ +``` + +Common sections include `Parameters`, `Returns`, `Raises`, `See Also`, +`Notes`, `References`, and `Examples`. Section names and indentation matter; +the headings should be followed by a line of hyphens. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..c419b72 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,92 @@ +# Getting started + +## Installation + +pySNOM supports Python 3.10 and newer. Install the package from the repository +root with: + +```console +python -m pip install . +``` + +The runtime dependencies are declared in `requirements.txt` and are installed +as part of the package installation. + +## First import + +The principal classes are available from the top-level package: + +```python +from pySNOM import Image, NeaInterferogram, NeaSpectrum, Reader +``` + +Reader implementations are available in `pySNOM.readers`, while image, +spectrum, and interferogram processing classes are documented in the API +reference. + +## Introduction + +pySNOM data pipelines generally follow the same two-step pattern: + +1. **Read** raw measurement data from disk with a `pySNOM.readers.Reader` + subclass, appropriate to the file format (Gwyddion `.gwy`, GSF `.gsf`, + or NeaSpec tab-separated text files). +2. **Process** the resulting data with a chain of `Transformation` objects + from `pySNOM.images` (for AFM/near-field images) or `pySNOM.spectra` + (for point, line, and hyperspectral spectra). + +Each `Transformation` implements a `transform(data)` method that takes and +returns a NumPy array, so transformations can be composed and applied in +sequence to build up a processing pipeline. + +## Image processing + +Read a single channel from a Gwyddion file with `GwyReader`, wrap it in an +`Image`, and apply a chain of transformations such as `LineLevel` and +`BackgroundPolyFit`: + +```python +from pySNOM.readers import GwyReader +from pySNOM.images import Image, LineLevel, BackgroundPolyFit, DataTypes + +reader = GwyReader(fullfilepath="measurement.gwy", channelname="O3A raw") +gwy_data = reader.read() + +image = Image(gwy_data, channelname="O3A raw") + +leveled = LineLevel(method="median", datatype=DataTypes.Amplitude) +image.data = leveled.transform(image.data) + +flattened = BackgroundPolyFit(xorder=1, yorder=1, datatype=DataTypes.Amplitude) +image.data = flattened.transform(image.data) +``` + +For stacks of images acquired at different wavenumbers (for example +hyperspectral scans), `pySNOM.images` also provides `AlignImageStack` and +related helpers (`create_nparray_stack`, `sort_image_stack`, +`dict_from_imagestack`) to align and reshape the stack before processing +each frame. + +## Spectral processing + +Read a NeaSpec spectrum file with `NeaSpectralReader` and wrap the resulting +data and parameters in a `NeaSpectrum`: + +```python +from pySNOM.readers import NeaSpectralReader +from pySNOM import spectra + +data, params = NeaSpectralReader("spectrum.txt").read() +spectrum = spectra.NeaSpectrum(data, params) + +print(spectrum.mode, spectrum.scantype) +``` + +Spectral transformations in `pySNOM.spectra` follow the same +`transform(data)` interface as image transformations, so channels can be +cut, scaled, and normalized in a pipeline, for example with `Cut`, `Scale`, +`LinearNormalize`, and `ShiftPhaseToZero`. + +See the API reference for the full list of readers and transformations +available for images, spectra, and interferograms. + diff --git a/docs/guide/images.md b/docs/guide/images.md new file mode 100644 index 0000000..e57a1e8 --- /dev/null +++ b/docs/guide/images.md @@ -0,0 +1,30 @@ +# Images + +Read a single channel from a Gwyddion file with `GwyReader`, wrap it in an +`Image`, and apply a chain of transformations such as `LineLevel` and +`BackgroundPolyFit`: + +```python +from pySNOM.readers import GwyReader +from pySNOM.images import Image, LineLevel, BackgroundPolyFit, DataTypes + +reader = GwyReader(fullfilepath="measurement.gwy", channelname="O3A raw") +gwy_data = reader.read() + +image = Image(gwy_data, channelname="O3A raw") + +leveled = LineLevel(method="median", datatype=DataTypes.Amplitude) +image.data = leveled.transform(image.data) + +flattened = BackgroundPolyFit(xorder=1, yorder=1, datatype=DataTypes.Amplitude) +image.data = flattened.transform(image.data) +``` + +For stacks of images acquired at different wavenumbers (for example +hyperspectral scans), `pySNOM.images` also provides `AlignImageStack` and +related helpers (`create_nparray_stack`, `sort_image_stack`, +`dict_from_imagestack`) to align and reshape the stack before processing +each frame. + +See the [API reference](../api/images.rst) for the full list of image +transformations available. diff --git a/docs/guide/index.md b/docs/guide/index.md new file mode 100644 index 0000000..9f26eee --- /dev/null +++ b/docs/guide/index.md @@ -0,0 +1,20 @@ +# User guide + +This is the user guide for the Python package `pySNOM`, which provides +readers and processing tools for Scanning Near-field Optical Microscopy +(SNOM) measurements, covering images, spectra, and interferograms. + +Here we provide examples of how to use this package, along with an +explanation of the typical read-then-process workflow. For detailed +documentation of the functions and classes provided by this package, see the +[API reference](../api/index.md). + +```{toctree} +:maxdepth: 2 + +introduction +readers +images +spectra +interferograms +``` diff --git a/docs/guide/interferograms.md b/docs/guide/interferograms.md new file mode 100644 index 0000000..36567b2 --- /dev/null +++ b/docs/guide/interferograms.md @@ -0,0 +1,17 @@ +# Interferograms + +`pySNOM.interferograms` provides `NeaInterferogram` and a family of +`ProcessSingleChannel`, `ProcessMultiChannels`, and `ProcessAllPoints` +transformations for turning raw interferogram data into spectra, along with +`InterpolateInterferogram` for resampling onto a regular grid. + +```python +from pySNOM.readers import NeaSpectralReader +from pySNOM.interferograms import NeaInterferogram + +data, params = NeaSpectralReader("interferogram.txt").read() +ifg = NeaInterferogram(data, params) +``` + +See the [API reference](../api/interferograms.rst) for the full list of +interferogram processing classes and helpers. diff --git a/docs/guide/introduction.md b/docs/guide/introduction.md new file mode 100644 index 0000000..2113583 --- /dev/null +++ b/docs/guide/introduction.md @@ -0,0 +1,18 @@ +# Introduction + +pySNOM data pipelines generally follow the same two-step pattern: + +1. **Read** raw measurement data from disk with a `pySNOM.readers.Reader` + subclass, appropriate to the file format (Gwyddion `.gwy`, GSF `.gsf`, + or NeaSpec tab-separated text files). +2. **Process** the resulting data with a chain of `Transformation` objects + from `pySNOM.images` (for AFM/near-field images), `pySNOM.spectra` (for + point, line, and hyperspectral spectra), or `pySNOM.interferograms` (for + raw interferogram data). + +Each `Transformation` implements a `transform(data)` method that takes and +returns a NumPy array, so transformations can be composed and applied in +sequence to build up a processing pipeline. + +See [Readers](readers.md), [Images](images.md), [Spectra](spectra.md), and +[Interferograms](interferograms.md) for worked examples of each stage. diff --git a/docs/guide/readers.md b/docs/guide/readers.md new file mode 100644 index 0000000..9a94c9b --- /dev/null +++ b/docs/guide/readers.md @@ -0,0 +1,21 @@ +# Readers + +`pySNOM.readers` provides `Reader` subclasses for the file formats produced +by common SNOM instruments and analysis tools: + +- `GwyReader` for Gwyddion (`.gwy`) files. +- `GsfReader` for Gwyddion Simple Field (`.gsf`) files. +- `NeaInfoReader` for NeaSpec info/header files. +- `NeaSpectralReader` for NeaSpec spectral and interferogram data files. +- `NeaFileLegacyReader` for legacy NeaSpec (`.nea`) files. +- `ImageStackXYZReader` for XYZ image stack files. + +```python +from pySNOM.readers import GwyReader + +reader = GwyReader(fullfilepath="measurement.gwy", channelname="O3A raw") +gwy_data = reader.read() +``` + +See the [API reference](../api/readers.rst) for the full list of readers and +their options. diff --git a/docs/guide/spectra.md b/docs/guide/spectra.md new file mode 100644 index 0000000..a46c949 --- /dev/null +++ b/docs/guide/spectra.md @@ -0,0 +1,22 @@ +# Spectra + +Read a NeaSpec spectrum file with `NeaSpectralReader` and wrap the resulting +data and parameters in a `NeaSpectrum`: + +```python +from pySNOM.readers import NeaSpectralReader +from pySNOM import spectra + +data, params = NeaSpectralReader("spectrum.txt").read() +spectrum = spectra.NeaSpectrum(data, params) + +print(spectrum.mode, spectrum.scantype) +``` + +Spectral transformations in `pySNOM.spectra` follow the same +`transform(data)` interface as image transformations, so channels can be +cut, scaled, and normalized in a pipeline, for example with `Cut`, `Scale`, +`LinearNormalize`, and `ShiftPhaseToZero`. + +See the [API reference](../api/spectra.rst) for the full list of spectral +transformations available. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..939441f --- /dev/null +++ b/docs/index.md @@ -0,0 +1,67 @@ +# pySNOM documentation + +Python tools for processing Scanning Near-field Optical Microscopy (SNOM) +measurements, including image, spectrum, and interferogram workflows. + +pySNOM provides readers and processing objects for working with experimental +SNOM data in Python. Start with the guide if you are new to the package, or go +directly to the API reference for detailed class and method documentation. + +
+ + + Start here +

Getting started

+

Install pySNOM and load your first measurement.

+
+ + + Fundamentals and usage +

User guide

+

Explore the functionality of pySNOM with the help of data processing examples and explanations.

+
+ + + Reference +

API reference

+

Explore readers, images, spectra, interferograms, and defaults.

+
+ + + Community +

Contribute

+

Report problems, request features, or help improve pySNOM.

+
+
+ +## Cite pySNOM + +If you use ``pySNOM`` in your research, please acknowledge the package and its +contributors at least in one of the following ways. + +Cite our journal paper presenting pySNOM integrated with the Orange data mining framework to leverage the power of visual programming and machine learning: + +```text +Gergely Németh, Marko Toplak, Stuart Read, Raul de Oliveira Freitas, Ferenc Borondics +Robust and Democratic s-SNOM Data Analysis and Modeling in Quasar. +ACS Omega 9 June 2026; 11 (22): 32971–32980. +https://doi.org/10.1021/acsomega.6c02534 +``` + +Cite the Zenodo repository: + +```text +Gergely Nemeth, Marko Toplak & borondics. (2026). Quasars/pySNOM: X.Y.Z +(Version X.Y.Z)[Computer software]. Zenodo. https://doi.org/10.5281/zenodo.18034757 +``` + + +```{toctree} +:hidden: +:maxdepth: 2 + +getting-started +guide/index +api/index +development +``` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..32bb245 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +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/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..d843e7b --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,8 @@ +sphinx>=7.0 +ipython +myst-parser>=2.0 +pydata-sphinx-theme>=0.20 +sphinx-autobuild +sphinx-copybutton +sphinx-design +numpydoc diff --git a/pySNOM/defaults.py b/pySNOM/defaults.py index 54de1d3..77474c8 100644 --- a/pySNOM/defaults.py +++ b/pySNOM/defaults.py @@ -1,11 +1,31 @@ -""" This file contains a dictionary of definitions to create connection -between the manufacturer nomeclature and the naming used in the package """ +"""Definitions that map manufacturer names to pySNOM names. + +The dictionaries in this module translate values found in Neaspec metadata +files into the measurement-mode names used by pySNOM. +""" class Defaults: - # Neaspec names are taken from the Scan field of the info txt file + """Mappings between Neaspec metadata and pySNOM measurement modes. + + Attributes + ---------- + image_mode_defs : dict of str to str + Mapping from image ``Scan`` values to pySNOM image-mode names. + spectral_mode_defs : dict of str to str + Mapping from spectral ``Scan`` values to pySNOM spectral-mode names. + """ + def __init__(self) -> None: - self.image_mode_defs = { + """Create the default manufacturer-to-package mappings. + + Returns + ------- + None + This constructor initializes the mapping attributes in place. + """ + + self.image_mode_defs: dict[str, str] = { "AFM": "AFM", "2D (PsHet)": "PsHet", "Whitelight Imaging": "WLI", @@ -14,7 +34,7 @@ def __init__(self) -> None: "Contact Mode 2D": "ContactAFM", } - self.spectral_mode_defs = { + self.spectral_mode_defs: dict[str, str] = { "Fourier Scan": "nanoFTIR", "Pointspectroscopy PTE+": "PTE", "AFM-Raman/PL Scan (Tapping Mode)": "nanoRaman", diff --git a/pySNOM/interferograms.py b/pySNOM/interferograms.py index 8cc97f9..881a9a0 100644 --- a/pySNOM/interferograms.py +++ b/pySNOM/interferograms.py @@ -1,3 +1,11 @@ +"""Classes and helpers for processing nanoFTIR interferogram data. + +This module provides :class:`NeaInterferogram`, a container for raw +interferogram channels and their acquisition parameters, transformations for +interpolation and Fourier processing, and :class:`Tools` utilities for +reshaping data and analysing measurement steps. +""" + import numpy as np import copy from enum import Enum @@ -15,6 +23,40 @@ # INTERFEROGRAMS ------------------------------------------------------------------------------------------------------------------ class NeaInterferogram(NeaSpectrum): + """Container for raw nanoFTIR interferograms and acquisition parameters. + + Parameters + ---------- + data : dict + Dictionary mapping channel names to raw interferogram arrays. For + line scans and hyperscans, arrays are reshaped to + ``(PixelArea[0], PixelArea[1], PixelArea[2] * Averaging)``. + parameters : dict + Measurement parameter dictionary containing at least ``"PixelArea"`` + and ``"Averaging"`` entries. + scantype : str, optional + Name of a :data:`ScanTypes` member used when parameters do not resolve + the scan geometry. + filename : str, optional + Full path, including name, of the source file. + mode : str, optional + Name of a :data:`MeasurementModes` member used when parameters do not + resolve the measurement mode. + + Attributes + ---------- + data : dict + Reshaped interferogram channels. + filename : str or None + Source file path. + mode : str + Resolved measurement mode name. + scantype : str + Resolved scan geometry name. + parameters : dict + Measurement parameter dictionary. + """ + def __init__( self, data, parameters, scantype="Point", filename=None, mode="nanoFTIR" ): @@ -25,7 +67,7 @@ def __init__( @property def data(self): - """Property - data (dict with measurement channels)""" + """Dictionary containing the measurement channels.""" return self._data @data.setter @@ -34,7 +76,21 @@ def data(self, value): self._data = Tools.reshape_ifg_data(value, self._parameters) def add_channel(self, values, channelname): - """Adds a new channel to data dictionary""" + """Add a channel and reshape it to the interferogram layout. + + Parameters + ---------- + values : array_like + Flat channel values with a length matching the measurement + dimensions. + channelname : str + Name under which to store the channel. + + Raises + ------ + ValueError + If `channelname` already exists in :attr:`data`. + """ if channelname not in list(self._data.keys()): self._data[channelname] = np.reshape( values, @@ -50,11 +106,40 @@ def add_channel(self, values, channelname): # TRANSFORMATIONS ------------------------------------------------------------------------------------------------------------------ class Transformation: + """Base class for interferogram transformations.""" + def transform(self, data): + """Transform interferogram data. + + Parameters + ---------- + data : array_like + Interferogram data to transform. + + Raises + ------ + NotImplementedError + Always, unless overridden by a subclass. + """ raise NotImplementedError() class ProcessInterferogram(Transformation): + """Fourier transform one interpolated interferogram. + + Parameters + ---------- + apod : bool, optional + Apply an asymmetric apodization window before the Fourier transform. + windowtype : str, optional + Name of the window function in ``scipy.signal.windows``. + nzeros : int, optional + Zero-filling factor applied to the interferogram length. + wlpidx : int, optional + Index of the white-light position. If omitted, use the largest + absolute interferogram value. + """ + def __init__(self, apod=True, windowtype="blackmanharris", nzeros=4, wlpidx=None): self.apod = apod self.nzeros = nzeros @@ -62,6 +147,23 @@ def __init__(self, apod=True, windowtype="blackmanharris", nzeros=4, wlpidx=None self.windowtype = windowtype def transform(self, ifg, maxis): + """Calculate the positive-frequency complex spectrum. + + Parameters + ---------- + ifg : array_like + One-dimensional, uniformly sampled interferogram. + maxis : array_like + Mirror-position axis corresponding to `ifg`, in metres. + + Returns + ------- + complex ndarray + Positive-frequency complex spectrum after centering and FFT. + ndarray + Wavenumber axis corresponding to the returned spectrum, in + inverse centimetres. + """ # Find the location index of the WLP if self.wlpidx is None: self.wlpidx = np.argmax(np.abs(ifg)) @@ -86,12 +188,37 @@ def transform(self, ifg, maxis): class InterpolateInterferogram(Transformation): - """Reinterpolates the raw interferograms to a uniform grid.""" + """Reinterpolate raw interferograms to a uniform mirror-position grid. + + Parameters + ---------- + method : {"spline", "linear"}, optional + Interpolation method. ``"spline"`` uses + :class:`scipy.interpolate.CubicSpline`; ``"linear"`` uses + :func:`scipy.interpolate.interp1d`. + """ def __init__(self, method="spline"): self.method = method def transform(self, ifg, maxis): + """Interpolate one or more interferograms. + + Parameters + ---------- + ifg : ndarray + One- or two-dimensional interferogram data. Rows in a 2-D array + represent separate interferograms. + maxis : ndarray + Mirror-position axis or axes corresponding to `ifg`. + + Returns + ------- + tuple of ndarray + Interpolated interferograms and their uniform mirror-position + axis. Inputs with unsupported dimensionality are returned + unchanged. + """ # if np.iscomplex(ifg).any(): newifg = np.zeros(np.shape(ifg)) * complex(1j) # else: @@ -142,6 +269,27 @@ def transform(self, ifg, maxis): class ProcessSingleChannel(Transformation): + """Process one optical demodulation channel into a spectrum. + + Parameters + ---------- + order : int + Optical demodulation order. Channels ``O{order}A`` and ``O{order}P`` + are read from the interferogram. + method : {"abs", "real", "imag", "complex", "simple"}, optional + Representation used to combine amplitude and phase channels. + apod : bool, optional + Apply asymmetric apodization before the Fourier transform. + windowtype : str, optional + Name of the SciPy window function used for apodization. + nzeros : int, optional + Zero-filling factor for the Fourier transform. + interpmethod : {"spline", "linear"}, optional + Method used to make the mirror-position grid uniform. + simpleoutput : bool, optional + If true, return arrays instead of a :class:`NeaSpectrum`. + """ + def __init__( self, order, @@ -161,6 +309,21 @@ def __init__( self.simpleoutput = simpleoutput def transform(self, neaifg): # Load amplitude and phase of the given channel + """Process the selected channel of `neaifg`. + + Parameters + ---------- + neaifg : NeaInterferogram + Interferogram containing amplitude, phase, and mirror-position + channels for the requested order. + + Returns + ------- + tuple of ndarray or NeaSpectrum + If `simpleoutput` is true, return ``(amplitude, phase, + wavenumber)``. Otherwise return the processed data as a + :class:`pySNOM.spectra.NeaSpectrum`. + """ # Calculate the interferogram to process based on the given method channelA = f"O{self.order}A" @@ -241,6 +404,25 @@ def transform(self, neaifg): # Load amplitude and phase of the given channel class ProcessMultiChannels(Transformation): + """Process optical channels for demodulation orders zero through five. + + Parameters + ---------- + method : {"abs", "real", "imag", "complex", "simple"}, optional + Representation used for each amplitude/phase channel pair. + apod : bool, optional + Apply asymmetric apodization before each Fourier transform. + windowtype : str, optional + Name of the SciPy window function used for apodization. + nzeros : int, optional + Zero-filling factor for each Fourier transform. + interpmethod : {"spline", "linear"}, optional + Method used to make the mirror-position grid uniform. + simpleoutput : bool, optional + Intended to select array output; when false, return a + :class:`NeaSpectrum`. + """ + def __init__( self, method="complex", @@ -258,6 +440,19 @@ def __init__( self.simpleoutput = simpleoutput def transform(self, neaifg): + """Process all supported optical channels in `neaifg`. + + Parameters + ---------- + neaifg : NeaInterferogram + Interferogram containing optical amplitude and phase channels. + + Returns + ------- + NeaSpectrum + Spectrum containing amplitude and phase channels and a + ``"Wavenumber"`` axis. + """ spectrum_data = {} spectrum_parameters = copy.deepcopy(neaifg.parameters) @@ -292,6 +487,22 @@ def transform(self, neaifg): class ProcessAllPoints(Transformation): + """Process every spatial point in an interferogram scan. + + Parameters + ---------- + method : {"abs", "real", "imag", "complex", "simple"}, optional + Representation used to combine amplitude and phase channels. + apod : bool, optional + Apply asymmetric apodization before each Fourier transform. + windowtype : str, optional + Name of the SciPy window function used for apodization. + nzeros : int, optional + Zero-filling factor for each Fourier transform. + interpmethod : {"spline", "linear"}, optional + Method used to make the mirror-position grid uniform. + """ + def __init__( self, method="complex", @@ -307,6 +518,19 @@ def __init__( self.interpmethod = interpmethod def transform(self, neaifg): + """Process all available optical channels at every scan point. + + Parameters + ---------- + neaifg : NeaInterferogram + Point, line-scan, or hyperscan interferogram. + + Returns + ------- + NeaSpectrum + Spectrum data with spatial dimensions preserved and processed + amplitude, phase, and wavenumber channels. + """ if ( neaifg.parameters["PixelArea"][0] == 1 and neaifg.parameters["PixelArea"][1] == 1 @@ -393,11 +617,30 @@ def transform(self, neaifg): # TOOLS ------------------------------------------------------------------------------------------------------------------ class Tools: + """Utility methods for interferogram reshaping and analysis.""" + def __init__(self): pass @staticmethod def reshape_ifg_data(data, params): + """Reshape raw interferogram channels for spatial scans. + + Parameters + ---------- + data : dict + Channel names mapped to flat arrays. The dictionary is modified + in place for multi-point data. + params : dict + Measurement parameters containing ``"PixelArea"`` and + ``"Averaging"``. + + Returns + ------- + dict + The input dictionary, with multi-point channels reshaped to + ``(PixelArea[0], PixelArea[1], PixelArea[2] * Averaging)``. + """ if params["PixelArea"][1] != 1 or params["PixelArea"][0] != 1: for channel in list(data.keys()): data[channel] = np.reshape( @@ -414,12 +657,42 @@ def reshape_ifg_data(data, params): @staticmethod def reshape_linescan_interferogram(data, parameters): + """Reshape line-scan data into spatial points and interferogram depth. + + Parameters + ---------- + data : array_like + Raw line-scan values. + parameters : dict + Measurement parameters containing ``"PixelArea"``. + + Returns + ------- + ndarray + Array with shape ``(PixelArea[0], PixelArea[2])``. + """ return np.reshape( np.ravel(data), (parameters["PixelArea"][0], parameters["PixelArea"][2]) ) @staticmethod def asymmetric_window(npoints, centerindex=None, windowtype="blackmanharris"): + """Construct an asymmetric window centered at a white-light position. + + Parameters + ---------- + npoints : int + Number of samples in the output window. + centerindex : int, optional + Index of the window center. Defaults to the midpoint. + windowtype : str, optional + Name of a window function in ``scipy.signal.windows``. + + Returns + ------- + ndarray + Window with `npoints` samples. + """ if centerindex is None: centerindex = int(len(windowPart2) / 2) @@ -444,6 +717,19 @@ def asymmetric_window(npoints, centerindex=None, windowtype="blackmanharris"): @staticmethod def analyse_steps(maxis): + """Calculate mean step sizes and their spread for mirror positions. + + Parameters + ---------- + maxis : ndarray + Two-dimensional array whose rows contain mirror-position axes. + + Returns + ------- + tuple of ndarray + Mean step sizes and standard deviations of the step sizes, each + with shape ``(number_of_rows, 1)``. + """ stepsize = np.zeros((np.shape(maxis)[0], 1)) stepspread = np.zeros((np.shape(maxis)[0], 1)) for i in range(np.shape(maxis)[0]): diff --git a/pySNOM/readers.py b/pySNOM/readers.py index 9d00d8b..a90577f 100644 --- a/pySNOM/readers.py +++ b/pySNOM/readers.py @@ -1,3 +1,11 @@ +"""File readers for SNOM measurement data. + +This module provides the :class:`Reader` base class and its subclasses for +loading images (Gwyddion, GSF, XYZ image stacks), spectra (NeaSpec, XYZ +file formats), and their associated info/header files, plus a handful of +filename and info-file helper functions used by the readers. +""" + import gwyfile import gsffile import numpy as np @@ -8,16 +16,53 @@ class Reader: + """Base class for all pySNOM file readers. + + Parameters + ---------- + fullfilepath : str, optional + Full path (with name) of the file to read. + + Attributes + ---------- + filename : str or None + Full path (with name) of the file to read. + """ + def __init__(self, fullfilepath=None): self.filename = fullfilepath class GwyReader(Reader): + """Reader for Gwyddion (``.gwy``) files. + + Parameters + ---------- + fullfilepath : str, optional + Full path (with name) of the ``.gwy`` file to read. + channelname : str, optional + If given, :meth:`read` returns only this channel instead of all of them. + + Attributes + ---------- + channelname : str or None + Name of the single channel to extract, or ``None`` to return all channels. + """ + def __init__(self, fullfilepath=None, channelname=None): super().__init__(fullfilepath) self.channelname = channelname def read(self): + """Read the Gwyddion file and return its data field(s). + + Returns + ------- + dict or GwyDataField + A dictionary mapping channel names to :class:`gwyfile.objects.GwyDataField` + objects when :attr:`channelname` is ``None``; otherwise the single + requested :class:`gwyfile.objects.GwyDataField`. + """ # Returns a dictionary of all the channels gwyobj = gwyfile.load(self.filename) allchannels = gwyfile.util.get_datafields(gwyobj) @@ -31,10 +76,25 @@ def read(self): class GsfReader(Reader): + """Reader for Gwyddion Simple Field (``.gsf``) files. + + Parameters + ---------- + fullfilepath : str, optional + Full path (with name) of the ``.gsf`` file to read. + """ + def __init__(self, fullfilepath=None): super().__init__(fullfilepath) def read(self): + """Read the GSF file and wrap it in a :class:`gwyfile.objects.GwyDataField`. + + Returns + ------- + gwyfile.objects.GwyDataField + Data field built from the GSF data and its real size/offset metadata. + """ data, metadata = gsffile.read_gsf(self.filename) channel = gwyfile.objects.GwyDataField( data, @@ -50,11 +110,40 @@ def read(self): class NeaHeaderReader(Reader): + """Reader for the ``#``-commented header block of NeaSpec data files. + + Parameters + ---------- + fullfilepath : str, optional + Full path (with name) of the NeaSpec data file whose header is read. + """ + def __init__(self, fullfilepath=None): super().__init__(fullfilepath) @staticmethod def parseline(linestring, params={}): + """Parse a single ``#``-commented header line into `params`. + + Recognizes several NeaSpec-specific field names (scanner center + position, scan/pixel area, averaging, interferometer center + distance, regulator settings, Q-factor) and stores them with + appropriately typed values; any other field is parsed as a float + when possible, otherwise kept as a stripped string. + + Parameters + ---------- + linestring : str + A single tab-separated header line, starting with ``"# "``. + params : dict, optional + Dictionary of parameters to update in place with the parsed + field. Mutated and returned. + + Returns + ------- + dict + The `params` dictionary, updated with the newly parsed field. + """ ct = linestring.split("\t") fieldname = ct[0][2:-1] fieldname = fieldname.replace(" ", "") @@ -106,6 +195,15 @@ def parseline(linestring, params={}): return params def read(self): + """Read the header block and the channel name row that follows it. + + Returns + ------- + tuple[list of str, dict] + A `(channels, params)` pair: the list of channel names found on + the first non-comment line, and the parameter dictionary parsed + from the preceding ``#``-commented header lines. + """ params = {} with open(self.filename, encoding="utf8") as f: # Read www.neaspec.com @@ -129,20 +227,65 @@ def read(self): class NeaInfoReader(NeaHeaderReader): + """Reader that extracts only the parameter dictionary from a NeaSpec info file. + + Parameters + ---------- + fullfilepath : str, optional + Full path (with name) of the NeaSpec info file to read. + """ + def __init__(self, fullfilepath=None): super().__init__(fullfilepath) def read(self): + """Read the info file and return its parameter dictionary. + + Returns + ------- + dict + Measurement parameters parsed from the info file's header. + """ _, infodict = super().read() return infodict class NeaSpectralReader(Reader): + """Reader for NeaSpec spectral data files (tab-separated, with a header block). + + Parameters + ---------- + fullfilepath : str, optional + Full path (with name) of the spectral data file to read. + output : {"dict", "dataframe"}, optional + Output format for the channel data returned by :meth:`read`. Any + value other than ``"dict"`` returns the raw :class:`pandas.DataFrame`. + + Attributes + ---------- + filename : str or None + Full path (with name) of the spectral data file to read. + """ + def __init__(self, fullfilepath=None, output="dict"): super().__init__(fullfilepath) self._output = output def read(self): + """Read the spectral data file. + + Parses the header via :class:`NeaHeaderReader` to determine the + channel names and the number of rows to skip, then reads the + remaining tab-separated data. + + Returns + ------- + tuple[dict or pandas.DataFrame, dict] + A `(data, params)` pair: the channel data (as a dictionary of + NumPy arrays when :attr:`_output` is ``"dict"``, otherwise as a + :class:`pandas.DataFrame`), and the measurement parameter + dictionary parsed from the header. + """ data = {} channels, params = NeaHeaderReader(self.filename).read() @@ -187,6 +330,26 @@ def __init__(self, folder=None, folder_pattern=""): self.folder_pattern = folder_pattern def read(self, pattern): + """Load and wavelength-sort a stack of GSF images matching `pattern`. + + Locates matching files via :func:`get_filenames`, reads each with + :class:`GsfReader`, and determines a wavelength/index for sorting + from the corresponding info file (falling back to the enumeration + index if the info file is missing or invalid). + + Parameters + ---------- + pattern : str + Regular expression matched against filenames within the + matching subfolders. + + Returns + ------- + tuple[list, list] + A `(imagestack, wns)` pair: the list of image data fields + (as read by :class:`GsfReader`), and the corresponding list of + wavelengths (or fallback indices), both sorted by wavelength. + """ imagestack = [] wns = [] filepaths = get_filenames( @@ -214,7 +377,23 @@ def read(self, pattern): def get_wl_from_infofile(infodict: dict): - """Returns the wavelength from the info file. If not found, returns 0.0""" + """Extract the measurement wavelength/wavenumber from a NeaSpec info dictionary. + + Uses the ``"TargetWavelength"`` entry when set, falling back to the + ``"InterferometerCenterDistance"`` entry otherwise. Values below 50 + (assumed to be in micrometres) are converted to wavenumbers (cm\\ :sup:`-1`). + + Parameters + ---------- + infodict : dict + Measurement parameter dictionary, as returned by :class:`NeaInfoReader`. + + Returns + ------- + float or None + The resolved wavelength/wavenumber, or ``None`` if it could not be + determined from `infodict`. + """ try: if infodict["TargetWavelength"] == "": wn = infodict["InterferometerCenterDistance"][0] @@ -230,7 +409,21 @@ def get_wl_from_infofile(infodict: dict): def get_wl_from_filename(filename): - """Returns the wavelength from the filename. If not found, returns None""" + """Extract a wavenumber value embedded in a filename. + + Looks for a number immediately preceded by ``_`` or ``-`` and followed + by a ``cm-1``/``cm_1`` unit suffix (e.g. ``"...-1234.5_cm-1.gsf"``). + + Parameters + ---------- + filename : str + File name or path to search; only the final path component is used. + + Returns + ------- + float or None + The extracted wavenumber, or ``None`` if no match was found. + """ wn = re.findall( r"(?<=[_-])(\d+(?:\.\d+)?)(?=(?:_?cm[-_]1|-?cm[-_]1))", PurePath(filename).name @@ -245,7 +438,25 @@ def get_wl_from_filename(filename): def get_filenames(folder: str, pattern: str, folderpattern=""): - """Returns the filepath of all files in the subfolders of the specified folder that contain pattern string in the filename""" + """Find files within matching subfolders of `folder` whose names match `pattern`. + + Parameters + ---------- + folder : str + Parent folder whose immediate subfolders are searched. + pattern : str + Regular expression that a subfolder's file names must match to be + included. + folderpattern : str, optional + Regular expression that subfolder names must match to be searched + at all. Defaults to matching every subfolder. + + Returns + ------- + list of str + Paths (relative to `folder`) of all matching files, in the order + they were found. + """ filepaths = [] @@ -262,7 +473,22 @@ def get_filenames(folder: str, pattern: str, folderpattern=""): def recreate_infofile_name_from_path(filepath: str): - """Recreates the name of the info file from the path of the data file""" + """Derive the info-file (``.txt``) path corresponding to a data file path. + + The info file is assumed to be named after the data file's parent + directory, placed one level up (NeaSpec's standard measurement folder + layout). + + Parameters + ---------- + filepath : str + Path of a data file inside a NeaSpec measurement subfolder. + + Returns + ------- + str + Path of the corresponding info file. + """ pathparts = list(PurePath(filepath).parts) newparts = pathparts[:-1] @@ -272,12 +498,32 @@ def recreate_infofile_name_from_path(filepath: str): class NeaFileLegacyReader(Reader): - """Reader for .nea files from older neasnom microscopes""" + """Reader for legacy ``.nea`` files from older NeaSpec microscopes. + + Parameters + ---------- + fullfilepath : str, optional + Full path (with name) of the legacy ``.nea`` file to read. + """ def __init__(self, fullfilepath=None): super().__init__(fullfilepath) def read(self): + """Read a legacy ``.nea`` file into per-channel data and scan parameters. + + Parses the tab-separated file's header and metadata columns (Row, + Column, Run, Channel, ...) to reconstruct each optical/mechanical + channel as a flat array indexed consistently with the scan's pixel + area, run count and spectral (omega) depth. + + Returns + ------- + tuple[dict, dict] + A `(data, params)` pair: a dictionary mapping channel (and + metadata) names to flat NumPy arrays, and a parameter + dictionary containing at least ``"PixelArea"`` and ``"Scan"``. + """ data = {} params = {} @@ -355,12 +601,36 @@ def read(self): class ImageStackXYZReader(Reader): - """Reads a list of images from the subfolders of the specified folder by loading the files that contain the pattern string int the filename""" + """Reader for XYZ-format image stack files (tab-separated, one column per spectral slice). + + Parameters + ---------- + fullfilepath : str, optional + Full path (with name) of the XYZ image stack file to read. + """ def __init__(self, fullfilepath=None): super().__init__(fullfilepath) def read(self): + """Read an XYZ-format image stack file into a list of 2D images. + + Parses the tab-separated file's header row as the spectral axis + (e.g. wavenumbers) and its ``Row``/``Column`` metadata columns to + reshape each spectral slice of the data into a 2D image. + + Returns + ------- + tuple[list of numpy.ndarray, numpy.ndarray or None] + A `(image_stack, x)` pair: the list of 2D images (one per + spectral axis value), and the parsed spectral axis values `x` + (``None`` if the header could not be parsed as floats). + + Raises + ------ + ValueError + If no file path was provided (:attr:`filename` is ``None``). + """ if self.filename is None: raise ValueError("No folder specified") else: diff --git a/pySNOM/spectra.py b/pySNOM/spectra.py index dfc821e..2ef94fe 100644 --- a/pySNOM/spectra.py +++ b/pySNOM/spectra.py @@ -1,18 +1,72 @@ +"""Classes and helpers for handling nanoFTIR/PsHet spectral measurement data. + +This module provides the :class:`NeaSpectrum` and :class:`SingleChannelSpectrum` +containers for spectral measurement data and parameters, a family of +:class:`Transformation` subclasses implementing common spectral processing +steps (cutting, scaling, normalizing, phase rotation and levelling), and the +:class:`Tools` helper class used to reshape raw spectral data arrays. +""" + import numpy as np from enum import Enum from pySNOM.images import type_from_channelname from pySNOM.defaults import Defaults +#: Supported spectral measurement modes. MeasurementModes = Enum( "MeasurementModes", ["None", "nanoFTIR", "PsHet", "PTE", "nanoRaman"] ) +#: Kinds of data a spectral channel can hold. DataTypes = Enum("DataTypes", ["Amplitude", "Phase", "Complex", "Topography"]) +#: Whether a channel is an optical or mechanical measurement channel. ChannelTypes = Enum("ChannelTypes", ["None", "Optical", "Mechanical"]) +#: Supported spectral scan geometries. +ScanTypes = Enum("ScanTypes", ["Point", "LineScan", "HyperScan"]) ScanTypes = Enum("ScanTypes", ["Point", "LineScan", "HyperScan"]) class NeaSpectrum: - """Storing full spectral measurement data and corresponding measurement parameters""" + """Container for full spectral measurement data and its acquisition parameters. + + Parameters + ---------- + data : dict + Dictionary mapping channel names to their raw spectral data arrays. + The arrays are reshaped in-place (see :meth:`Tools.reshape_spectrum_data`) + according to `parameters`. + parameters : dict + Measurement parameter dictionary as read from the info file (e.g. + containing ``"PixelArea"`` and ``"Scan"`` entries). When provided and + truthy, `scantype` and `mode` are derived from it instead of the + given arguments. + scantype : str, optional + Name of a :data:`ScanTypes` member describing the scan geometry, + used only when `parameters` is empty/``None``. + filename : str, optional + Full path (with name) of the file the spectrum was loaded from. + mode : str, optional + Name of a :data:`MeasurementModes` member, used only when + `parameters` is empty/``None``. + + Attributes + ---------- + filename : str or None + Full path with name of the source file. + data : dict + Reshaped measurement channel data. + mode : str + Name of the resolved :data:`MeasurementModes` member. + scantype : str + Name of the resolved :data:`ScanTypes` member. + parameters : dict + Measurement parameter dictionary. + + Raises + ------ + ValueError + If `mode` or `scantype` is not a valid enum member name, or if + `parameters` is provided but does not contain the expected keys. + """ def __init__( self, @@ -92,7 +146,30 @@ def parameters(self): return self._parameters def add_channel(self, values, channelname, zerofilling=1): - """Adds a new channel to data dictionary""" + """Add a new channel to the data dictionary. + + The flat `values` array is reshaped to match the pixel area recorded + in :attr:`parameters`, taking `zerofilling` into account for the + spectral axis length. + + Parameters + ---------- + values : array_like + Flat array of channel values to store, whose length must equal + the product of the pixel area dimensions (scaled by + `zerofilling` along the spectral axis). + channelname : str + Name under which the channel is stored in :attr:`data`. Must not + already exist in the data dictionary. + zerofilling : int, optional + Zero-filling factor applied to the spectral axis length when + reshaping `values`. + + Raises + ------ + ValueError + If `channelname` already exists in :attr:`data`. + """ if channelname not in list(self._data.keys()): self._data[channelname] = np.reshape( values, @@ -106,6 +183,40 @@ def add_channel(self, values, channelname, zerofilling=1): raise ValueError class SingleChannelSpectrum(NeaSpectrum): + """A single measurement channel paired with its wavenumber axis. + + Parameters + ---------- + data : dict + Dictionary of measurement channel data, as accepted by + :class:`NeaSpectrum`. + wndata : array_like + Wavenumber axis values corresponding to the spectral data. + filename : str, optional + Full path (with name) of the file the spectrum was loaded from. + parameters : dict, optional + Measurement parameter dictionary, as accepted by :class:`NeaSpectrum`. + mode : str, optional + Name of a :data:`MeasurementModes` member. + channelname : str, optional + Name of the channel represented by this instance. Determines + :attr:`channeltype`, :attr:`order` and :attr:`datatype` via + :func:`pySNOM.images.type_from_channelname`. + + Attributes + ---------- + wndata : array_like + Wavenumber axis values corresponding to the spectral data. + channel : str + Name of the represented channel. + channeltype : ChannelTypes + Optical/mechanical classification of the channel. + order : int + Demodulation order extracted from the channel name. + datatype : DataTypes + Data kind (amplitude, phase, ...) extracted from the channel name. + """ + def __init__( self, data, @@ -144,25 +255,113 @@ def channel(self, value): # TRANSFORMATIONS ------------------------------------------------------------------------------------------------------------------ class Transformation: + """Base class for spectral data transformations. + + Subclasses implement :meth:`transform` to process a spectrum (and, + typically, its wavenumber axis) and return the transformed result. + """ + def transform(self, data): + """Transform the given spectral data. + + Parameters + ---------- + data : array_like + Spectral data to transform. + + Returns + ------- + array_like + The transformed spectral data. + + Raises + ------ + NotImplementedError + Always, unless overridden by a subclass. + """ raise NotImplementedError() class Cut(Transformation): + """Crop a spectrum to the wavenumber range [`wavenumber1`, `wavenumber2`]. + + Parameters + ---------- + wavenumber1 : float, optional + Lower bound of the wavenumber range to keep. + wavenumber2 : float, optional + Upper bound of the wavenumber range to keep. + + Attributes + ---------- + wn1 : float + Lower bound of the wavenumber range to keep. + wn2 : float + Upper bound of the wavenumber range to keep. + """ + def __init__(self, wavenumber1=0.0, wavenumber2=1000.0): self.wn1 = wavenumber1 self.wn2 = wavenumber2 def transform(self, spectrum, wnaxis): + """Crop `spectrum` and `wnaxis` to the configured wavenumber range. + + Parameters + ---------- + spectrum : array_like + Spectral data values. + wnaxis : array_like + Wavenumber axis values corresponding to `spectrum`. + + Returns + ------- + tuple of array_like + The cropped `(spectrum, wnaxis)` pair, restricted to the indices + closest to `wn1` and `wn2`. + """ wn1idx = np.argmin(abs(wnaxis - self.wn1)) wn2idx = np.argmin(abs(wnaxis - self.wn2)) return spectrum[wn1idx:wn2idx], wnaxis[wn1idx:wn2idx] class Scale(Transformation): + """Scale spectral data by a constant factor, or rotate phase by it. + + Parameters + ---------- + factor : float, optional + Multiplicative scale factor. For phase data, interpreted as a + rotation applied via complex exponentiation. + datatype : DataTypes, optional + Kind of data being scaled; :data:`DataTypes.Phase` is handled via + complex rotation, other types via plain multiplication. + + Attributes + ---------- + factor : float + Multiplicative scale factor. + datatype : DataTypes + Kind of data being scaled. + """ + def __init__(self, factor=1.0, datatype=DataTypes.Phase): self.factor = factor self.datatype = datatype def transform(self, spectrum): + """Scale `spectrum` by :attr:`factor`. + + Parameters + ---------- + spectrum : array_like + Spectral data values. For :data:`DataTypes.Phase`, may be real + (radians) or complex; real values are converted to complex + before rotation. + + Returns + ------- + array_like + The scaled (or phase-rotated) spectrum. + """ if self.datatype == DataTypes.Phase: if not np.iscomplex(spectrum).any(): spectrum = np.exp(spectrum * self.factor * complex(1j)) @@ -172,12 +371,53 @@ def transform(self, spectrum): class LinearNormalize(Transformation): + """Subtract (or divide by) a linear baseline defined by two reference points. + + The baseline is the line through `(wavenumber1, spectrum[wn1])` and + `(wavenumber2, spectrum[wn2])`. + + Parameters + ---------- + wavenumber1 : float, optional + First reference wavenumber defining the baseline. + wavenumber2 : float, optional + Second reference wavenumber defining the baseline. + datatype : DataTypes, optional + Kind of data being normalized; :data:`DataTypes.Amplitude` divides + by the baseline, other types subtract it. + + Attributes + ---------- + wn1 : float + First reference wavenumber defining the baseline. + wn2 : float + Second reference wavenumber defining the baseline. + datatype : DataTypes + Kind of data being normalized. + """ + def __init__(self, wavenumber1=0.0, wavenumber2=1000.0, datatype=DataTypes.Phase): self.wn1 = wavenumber1 self.wn2 = wavenumber2 self.datatype = datatype def transform(self, spectrum, wnaxis): + """Remove the linear baseline from `spectrum`. + + Parameters + ---------- + spectrum : array_like + Spectral data values. + wnaxis : array_like + Wavenumber axis values corresponding to `spectrum`. + + Returns + ------- + array_like + The baseline-corrected spectrum, divided by the baseline for + :data:`DataTypes.Amplitude` data, otherwise with the baseline + subtracted. + """ wn1idx = np.argmin(abs(wnaxis - self.wn1)) wn2idx = np.argmin(abs(wnaxis - self.wn2)) m = (spectrum[wn2idx] - spectrum[wn1idx]) / (wnaxis[wn2idx] - wnaxis[wn1idx]) @@ -189,12 +429,57 @@ def transform(self, spectrum, wnaxis): return spectrum - (m * wnaxis + C) class ConstantNormalize(Transformation): + """Subtract (or divide by) a constant reference value. + + The reference value can be a fixed constant or read from the spectrum + itself at a given wavenumber. + + Parameters + ---------- + value : float, optional + Constant reference value, or the reference wavenumber when + `from_spectrum` is ``True``. + from_spectrum : bool, optional + If ``True``, the reference value is taken from `spectrum` at the + point closest to `value` (interpreted as a wavenumber) rather than + used directly as the reference value. + datatype : DataTypes, optional + Kind of data being normalized; :data:`DataTypes.Amplitude` divides + by the reference value, other types subtract it. + + Attributes + ---------- + value : float + Constant reference value or reference wavenumber. + from_spectrum : bool + Whether the reference value is read from the spectrum. + datatype : DataTypes + Kind of data being normalized. + """ + def __init__(self, value=1.0, from_spectrum=False, datatype=DataTypes.Phase): self.value = value self.from_spectrum = from_spectrum self.datatype = datatype def transform(self, spectrum, wnaxis): + """Subtract or divide `spectrum` by the configured reference value. + + Parameters + ---------- + spectrum : array_like + Spectral data values. + wnaxis : array_like + Wavenumber axis values corresponding to `spectrum`, used only + when :attr:`from_spectrum` is ``True``. + + Returns + ------- + array_like + The normalized spectrum, divided by the reference value for + :data:`DataTypes.Amplitude` data, otherwise with the reference + value subtracted. + """ if self.from_spectrum: data_idx = np.argmin(np.abs(wnaxis-self.value)) data_value = spectrum[data_idx] @@ -207,12 +492,53 @@ def transform(self, spectrum, wnaxis): return spectrum - data_value class RotatePhase(Transformation): + """Rotate the phase of a spectrum (equal to shifting), either linearly with wavenumber or by a constant. + + Parameters + ---------- + degree : float, optional + Rotation angle in degrees. When `constant_shift` is ``False``, this + is the rotation applied at `wn_ref` and scales linearly with + wavenumber; when ``True``, it is applied uniformly across the axis. + wn_ref : float, optional + Reference wavenumber at which `degree` applies, used only when + `constant_shift` is ``False``. + constant_shift : bool, optional + If ``True``, apply a constant phase shift of `degree` instead of a + wavenumber-dependent rotation. + + Attributes + ---------- + wn_ref : float + Reference wavenumber for the wavenumber-dependent rotation. + degree : float + Rotation angle in degrees. + constant_shift : bool + Whether a constant (as opposed to wavenumber-dependent) shift is applied. + """ + def __init__(self, degree=0.0, wn_ref=1000.0, constant_shift=False): self.wn_ref = wn_ref self.degree = degree self.constant_shift = constant_shift def transform(self, spectrum, wnaxis): + """Apply the configured phase rotation to `spectrum`. + + Parameters + ---------- + spectrum : array_like + Spectral data values. Real values are converted to complex + before rotation. + wnaxis : array_like + Wavenumber axis values corresponding to `spectrum`, used only + when :attr:`constant_shift` is ``False``. + + Returns + ------- + array_like + The phase (in radians) of the rotated spectrum. + """ if not np.iscomplex(spectrum).any(): spectrum = np.exp(spectrum * complex(1j)) @@ -235,6 +561,22 @@ def __init__(self, wavenumber1=1000.0, wavenumber2=2200.0): self.wn2 = wavenumber2 def transform(self, spectrum, wnaxis): + """Level the phase of `spectrum` using the two configured reference points. + + Parameters + ---------- + spectrum : array_like + Phase spectrum values. Real values are converted to complex + before rotation. + wnaxis : array_like + Wavenumber axis values corresponding to `spectrum`. + + Returns + ------- + array_like + The rotated phase spectrum (in radians), flat and equal to zero + at `wavenumber1` and `wavenumber2`. + """ if not np.iscomplex(spectrum).any(): spectrum = np.exp(spectrum * complex(1j)) @@ -255,11 +597,47 @@ def transform(self, spectrum, wnaxis): class NormalizeSpectrum(Transformation): + """Normalize a spectrum against a reference spectrum. + + Parameters + ---------- + datatype : DataTypes, optional + Kind of data being normalized; :data:`DataTypes.Phase` and + :data:`DataTypes.Topography` are treated as phase-like data and + normalized via complex division, other types via plain division. + dounwrap : bool, optional + If ``True`` and `datatype` is phase-like, unwrap the resulting phase + spectrum. + + Attributes + ---------- + datatype : DataTypes + Kind of data being normalized. + dounwrap : bool + Whether to unwrap the resulting phase spectrum. + """ + def __init__(self, datatype=DataTypes.Phase, dounwrap=False): self.datatype = datatype self.dounwrap = dounwrap def transform(self, spectrum, refspectrum): + """Normalize `spectrum` against `refspectrum`. + + Parameters + ---------- + spectrum : array_like + Spectral data values to normalize. + refspectrum : array_like + Reference spectral data values, same shape as `spectrum`. + + Returns + ------- + array_like + The normalized spectrum: for phase-like data, the (optionally + unwrapped) phase difference between `spectrum` and + `refspectrum`; otherwise the elementwise ratio. + """ if self.datatype == DataTypes.Phase or self.datatype == DataTypes.Topography: newspectrum = np.angle( np.exp(spectrum * complex(1j)) / np.exp(refspectrum * complex(1j)) @@ -274,8 +652,35 @@ def transform(self, spectrum, refspectrum): # TOOLS ------------------------------------------------------------------------------------------------------------------ class Tools: + """Helper functions for working with raw spectral measurement data.""" + @staticmethod def reshape_spectrum_data(data, params): + """Reshape flat channel arrays into point/linescan/hyperscan layouts. + + Compensates for the zero-filling NeaSpec applies to Fourier-scan + interferograms, and infers the spectral axis length from any + available axis channel (``"Depth"``, ``"Index"``, ``"Omega"``, + ``"Wavenumber"`` or ``"Wavelength"``), falling back to the pixel + area recorded in `params`. + + Parameters + ---------- + data : dict + Dictionary mapping channel names to flat arrays of raw values. + Reshaped in place. + params : dict + Measurement parameter dictionary containing at least ``"Scan"`` + and ``"PixelArea"`` entries. + + Returns + ------- + dict + The same `data` dictionary, with every channel reshaped to + ``(spectral_depth,)`` for point spectra, or + ``(PixelArea[0], PixelArea[1], spectral_depth)`` for line/hyper + scans. + """ # To compensate for the zero-filling that NeaSpec does n = 1 if params["Scan"] == "Fourier Scan":