├── .gitignore ├── LICENSE ├── NEWS.rst ├── README.rst ├── doc ├── api.rst ├── conf.py ├── examples.rst ├── examples │ ├── MRT01.sofa │ ├── MRT01.txt │ ├── MRT01_source.txt │ └── SOFA-file-access.ipynb ├── index.rst ├── requirements.txt └── version-history.rst ├── setup.cfg ├── setup.py └── src └── sofa ├── __init__.py ├── _database.py ├── access ├── __init__.py ├── dimensions.py ├── metadata.py ├── proxy.py └── variables.py ├── conventions ├── GeneralFIR.py ├── GeneralFIRE.py ├── GeneralTF.py ├── MultiSpeakerBRIR.py ├── SimpleFreeFieldHRIR.py ├── SimpleFreeFieldSOS.py ├── SimpleFreeFieldTF.py ├── SimpleHeadphoneIR.py ├── SingleRoomDRIR.py ├── __init__.py └── base.py ├── datatypes ├── FIR.py ├── FIRE.py ├── SOS.py ├── TF.py ├── __init__.py └── base.py ├── roomtypes ├── __init__.py ├── base.py ├── freefield.py ├── reverberant.py └── shoebox.py └── spatial ├── __init__.py ├── coordinates.py └── spatialobject.py /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__/ 2 | **/.ipynb_checkpoints/ 3 | doc/examples/free_field_HRIR.sofa 4 | build/ 5 | dist/ 6 | sofa.egg-info/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jannika Lossner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NEWS.rst: -------------------------------------------------------------------------------- 1 | Version History 2 | =============== 3 | Version 0.2.0 (2020-03-03): 4 | - Switched spherical coordinate definition from azimuth and colatitude (0...180) angles to azimuth and elevation (90...-90) to conform to SOFA specifications. 5 | - Provided a more direct access to variable and attribute creation within the dataset 6 | - Reworked the initialization process and DataType creation 7 | - Reworked RoomTypes 8 | - Ensured conventions conform to SOFA 1.0 9 | - Updated usage example 10 | 11 | Version 0.1.2 (2020-02-25): 12 | - Fixed issues in relative coordinate access. 13 | 14 | Version 0.1.1 (2020-01-23): 15 | - Fixed issues in array accessing and coordinate system conversion. 16 | 17 | Version 0.1.0 (2019-07-03): 18 | - Initial release. 19 | 20 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Spatially Oriented Format for Acoustics (SOFA) API for Python 2 | ============================================================= 3 | A Python API for reading, writing and creating SOFA files as defined by the SOFA conventions (version 1.0) found at https://www.sofaconventions.org/. 4 | 5 | Documentation: 6 | https://python-sofa.readthedocs.io/ 7 | 8 | Source code and issue tracker: 9 | https://github.com/spatialaudio/python-sofa/ 10 | 11 | License: 12 | MIT -- see the file ``LICENSE`` for details. 13 | 14 | Quick start: 15 | * Install Python 3 16 | * ``python3 -m pip install python-sofa --user`` 17 | * Check out the examples in the documentation 18 | 19 | -------------------------------------------------------------------------------- /doc/api.rst: -------------------------------------------------------------------------------- 1 | API Documentation 2 | ================= 3 | 4 | .. automodule:: sofa 5 | :members: 6 | 7 | sofa.access 8 | ----------- 9 | .. automodule:: sofa.access 10 | :inherited-members: 11 | :members: 12 | 13 | sofa.conventions 14 | ---------------- 15 | .. automodule:: sofa.conventions 16 | :members: 17 | 18 | sofa.datatypes 19 | -------------- 20 | .. automodule:: sofa.datatypes 21 | :inherited-members: 22 | :members: 23 | 24 | sofa.roomtypes 25 | -------------- 26 | .. automodule:: sofa.roomtypes 27 | :inherited-members: 28 | :members: 29 | 30 | sofa.spatial 31 | ------------ 32 | .. automodule:: sofa.spatial 33 | :members: 34 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # python-sofa documentation build configuration file. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | from subprocess import check_output 18 | 19 | import sphinx 20 | 21 | # If extensions (or modules to document with autodoc) are in another directory, 22 | # add these directories to sys.path here. If the directory is relative to the 23 | # documentation root, use os.path.abspath to make it absolute, like shown here. 24 | sys.path.insert(0, os.path.abspath('../src')) 25 | 26 | # -- General configuration ------------------------------------------------ 27 | 28 | # If your documentation needs a minimal Sphinx version, state it here. 29 | needs_sphinx = '1.3' # for sphinx.ext.napoleon 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | 'sphinx.ext.autodoc', 36 | 'sphinx.ext.autosummary', 37 | 'sphinx.ext.mathjax', 38 | 'sphinx.ext.viewcode', 39 | 'sphinx.ext.napoleon', # support for NumPy-style docstrings 40 | 'sphinx.ext.intersphinx', 41 | 'sphinx.ext.doctest', 42 | 'sphinxcontrib.bibtex', 43 | 'sphinx.ext.extlinks', 44 | 'matplotlib.sphinxext.plot_directive', 45 | 'nbsphinx', 46 | ] 47 | 48 | nbsphinx_execute_arguments = [ 49 | "--InlineBackend.figure_formats={'svg', 'pdf'}", 50 | "--InlineBackend.rc={'figure.dpi': 96}", 51 | ] 52 | 53 | # Tell autodoc that the documentation is being generated 54 | sphinx.SOFA_DOCS_ARE_BEING_BUILT = True 55 | 56 | autoclass_content = 'init' 57 | autodoc_member_order = 'bysource' 58 | autodoc_default_flags = ['members', 'undoc-members'] 59 | 60 | autosummary_generate = ['api', 'examples'] 61 | 62 | napoleon_google_docstring = False 63 | napoleon_numpy_docstring = True 64 | napoleon_include_private_with_doc = False 65 | napoleon_include_special_with_doc = False 66 | napoleon_use_admonition_for_examples = False 67 | napoleon_use_admonition_for_notes = False 68 | napoleon_use_admonition_for_references = False 69 | napoleon_use_ivar = False 70 | napoleon_use_param = False 71 | napoleon_use_rtype = False 72 | 73 | intersphinx_mapping = { 74 | 'python': ('https://docs.python.org/3/', None), 75 | 'numpy': ('https://docs.scipy.org/doc/numpy/', None), 76 | 'scipy': ('https://docs.scipy.org/doc/scipy/reference/', None), 77 | 'matplotlib': ('https://matplotlib.org/', None), 78 | } 79 | 80 | #extlinks = {'python-sofa': ('https://sfs.readthedocs.io/en/3.2/%s', 81 | # 'https://sfs.rtfd.io/')} 82 | 83 | plot_include_source = True 84 | plot_html_show_source_link = False 85 | plot_html_show_formats = False 86 | plot_pre_code = '' 87 | plot_rcparams = { 88 | 'savefig.bbox': 'tight', 89 | } 90 | plot_formats = ['svg', 'pdf'] 91 | 92 | mathjax_config = { 93 | 'TeX': { 94 | 'extensions': ['newcommand.js', 'begingroup.js'], # Support for \gdef 95 | }, 96 | } 97 | 98 | # Add any paths that contain templates here, relative to this directory. 99 | templates_path = ['_template'] 100 | 101 | # The suffix of source filenames. 102 | source_suffix = '.rst' 103 | 104 | # The encoding of source files. 105 | #source_encoding = 'utf-8-sig' 106 | 107 | # The master toctree document. 108 | master_doc = 'index' 109 | 110 | # General information about the project. 111 | authors = 'Jannika Lossner' 112 | project = 'PythonSOFA' 113 | copyright = '2019, ' + authors 114 | 115 | # The version info for the project you're documenting, acts as replacement for 116 | # |version| and |release|, also used in various other places throughout the 117 | # built documents. 118 | # 119 | # The short X.Y version. 120 | #version = '0.0.0' 121 | # The full version, including alpha/beta/rc tags. 122 | try: 123 | release = check_output(['git', 'describe', '--tags', '--always']) 124 | release = release.decode().strip() 125 | except Exception: 126 | release = '' 127 | 128 | # The language for content autogenerated by Sphinx. Refer to documentation 129 | # for a list of supported languages. 130 | #language = None 131 | 132 | # There are two options for replacing |today|: either, you set today to some 133 | # non-false value, then it is used: 134 | #today = '' 135 | # Else, today_fmt is used as the format for a strftime call. 136 | #today_fmt = '%B %d, %Y' 137 | try: 138 | today = check_output(['git', 'show', '-s', '--format=%ad', '--date=short']) 139 | today = today.decode().strip() 140 | except Exception: 141 | today = '' 142 | 143 | # List of patterns, relative to source directory, that match files and 144 | # directories to ignore when looking for source files. 145 | exclude_patterns = ['_build', '**/.ipynb_checkpoints'] 146 | 147 | # The reST default role (used for this markup: `text`) to use for all 148 | # documents. 149 | default_role = 'any' 150 | 151 | # If true, '()' will be appended to :func: etc. cross-reference text. 152 | #add_function_parentheses = True 153 | 154 | # If true, the current module name will be prepended to all description 155 | # unit titles (such as .. function::). 156 | #add_module_names = True 157 | 158 | # If true, sectionauthor and moduleauthor directives will be shown in the 159 | # output. They are ignored by default. 160 | #show_authors = False 161 | 162 | # The name of the Pygments (syntax highlighting) style to use. 163 | pygments_style = 'sphinx' 164 | 165 | # A list of ignored prefixes for module index sorting. 166 | #modindex_common_prefix = [] 167 | 168 | # If true, keep warnings as "system message" paragraphs in the built documents. 169 | #keep_warnings = False 170 | 171 | jinja_define = """ 172 | {% set docname = env.doc2path(env.docname, base='doc') %} 173 | {% set latex_href = ''.join([ 174 | '\href{https://github.com/sfstoolbox/sfs-python/blob/', 175 | env.config.release, 176 | '/', 177 | docname | escape_latex, 178 | '}{\sphinxcode{\sphinxupquote{', 179 | docname | escape_latex, 180 | '}}}', 181 | ]) %} 182 | """ 183 | 184 | nbsphinx_prolog = jinja_define + r""" 185 | .. only:: html 186 | 187 | .. role:: raw-html(raw) 188 | :format: html 189 | 190 | .. nbinfo:: 191 | 192 | This page was generated from `{{ docname }}`__. 193 | Interactive online version: 194 | :raw-html:`Binder badge` 195 | 196 | __ https://github.com/sfstoolbox/sfs-python/blob/ 197 | {{ env.config.release }}/{{ docname }} 198 | 199 | .. raw:: latex 200 | 201 | \nbsphinxstartnotebook{\scriptsize\noindent\strut 202 | \textcolor{gray}{The following section was generated from {{ latex_href }} 203 | \dotfill}} 204 | """ 205 | 206 | nbsphinx_epilog = jinja_define + r""" 207 | .. raw:: latex 208 | 209 | \nbsphinxstopnotebook{\scriptsize\noindent\strut 210 | \textcolor{gray}{\dotfill\ {{ latex_href }} ends here.}} 211 | """ 212 | 213 | 214 | # -- Options for HTML output ---------------------------------------------- 215 | 216 | def setup(app): 217 | """Include custom theme files to sphinx HTML header""" 218 | app.add_stylesheet('css/title.css') 219 | 220 | # The theme to use for HTML and HTML Help pages. See the documentation for 221 | # a list of builtin themes. 222 | html_theme = 'sphinx_rtd_theme' 223 | 224 | # Theme options are theme-specific and customize the look and feel of a theme 225 | # further. For a list of options available for each theme, see the 226 | # documentation. 227 | html_theme_options = { 228 | 'collapse_navigation': False, 229 | } 230 | 231 | # Add any paths that contain custom themes here, relative to this directory. 232 | #html_theme_path = [] 233 | 234 | # The name for this set of Sphinx documents. If None, it defaults to 235 | # " v documentation". 236 | html_title = project + ", version " + release 237 | 238 | # A shorter title for the navigation bar. Default is the same as html_title. 239 | #html_short_title = None 240 | 241 | # The name of an image file (relative to this directory) to place at the top 242 | # of the sidebar. 243 | #html_logo = None 244 | 245 | # The name of an image file (within the static path) to use as favicon of the 246 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 247 | # pixels large. 248 | #html_favicon = None 249 | 250 | # Add any paths that contain custom static files (such as style sheets) here, 251 | # relative to this directory. They are copied after the builtin static files, 252 | # so a file named "default.css" will overwrite the builtin "default.css". 253 | html_static_path = ['_static'] 254 | 255 | # Add any extra paths that contain custom files (such as robots.txt or 256 | # .htaccess) here, relative to this directory. These files are copied 257 | # directly to the root of the documentation. 258 | #html_extra_path = [] 259 | 260 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 261 | # using the given strftime format. 262 | #html_last_updated_fmt = '%b %d, %Y' 263 | 264 | # If true, SmartyPants will be used to convert quotes and dashes to 265 | # typographically correct entities. 266 | #html_use_smartypants = True 267 | 268 | # Custom sidebar templates, maps document names to template names. 269 | #html_sidebars = {} 270 | 271 | # Additional templates that should be rendered to pages, maps page names to 272 | # template names. 273 | #html_additional_pages = {} 274 | 275 | # If false, no module index is generated. 276 | #html_domain_indices = True 277 | 278 | # If false, no index is generated. 279 | #html_use_index = True 280 | 281 | # If true, the index is split into individual pages for each letter. 282 | #html_split_index = False 283 | 284 | # If true, links to the reST sources are added to the pages. 285 | html_show_sourcelink = True 286 | html_sourcelink_suffix = '' 287 | 288 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 289 | #html_show_sphinx = True 290 | 291 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 292 | #html_show_copyright = True 293 | 294 | # If true, an OpenSearch description file will be output, and all pages will 295 | # contain a tag referring to it. The value of this option must be the 296 | # base URL from which the finished HTML is served. 297 | #html_use_opensearch = '' 298 | 299 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 300 | #html_file_suffix = None 301 | 302 | # Output file base name for HTML help builder. 303 | htmlhelp_basename = 'SFS' 304 | 305 | html_scaled_image_link = False 306 | 307 | # -- Options for LaTeX output --------------------------------------------- 308 | 309 | latex_elements = { 310 | 'papersize': 'a4paper', 311 | 'printindex': '', 312 | 'sphinxsetup': r""" 313 | VerbatimColor={HTML}{F5F5F5}, 314 | VerbatimBorderColor={HTML}{E0E0E0}, 315 | noteBorderColor={HTML}{E0E0E0}, 316 | noteborder=1.5pt, 317 | warningBorderColor={HTML}{E0E0E0}, 318 | warningborder=1.5pt, 319 | warningBgColor={HTML}{FBFBFB}, 320 | """, 321 | 'preamble': r""" 322 | \usepackage[sc,osf]{mathpazo} 323 | \linespread{1.05} % see http://www.tug.dk/FontCatalogue/urwpalladio/ 324 | \renewcommand{\sfdefault}{pplj} % Palatino instead of sans serif 325 | \IfFileExists{zlmtt.sty}{ 326 | \usepackage[light,scaled=1.05]{zlmtt} % light typewriter font from lmodern 327 | }{ 328 | \renewcommand{\ttdefault}{lmtt} % typewriter font from lmodern 329 | } 330 | """, 331 | } 332 | 333 | # Grouping the document tree into LaTeX files. List of tuples 334 | # (source start file, target name, title, 335 | # author, documentclass [howto, manual, or own class]). 336 | latex_documents = [('index', 'SFS.tex', project, authors, 'howto')] 337 | 338 | # The name of an image file (relative to this directory) to place at the top of 339 | # the title page. 340 | #latex_logo = None 341 | 342 | # For "manual" documents, if this is true, then toplevel headings are parts, 343 | # not chapters. 344 | #latex_use_parts = False 345 | 346 | # If true, show page references after internal links. 347 | #latex_show_pagerefs = False 348 | 349 | # If true, show URL addresses after external links. 350 | latex_show_urls = 'footnote' 351 | 352 | # Documents to append as an appendix to all manuals. 353 | #latex_appendices = [] 354 | 355 | # If false, no module index is generated. 356 | latex_domain_indices = False 357 | 358 | 359 | # -- Options for manual page output --------------------------------------- 360 | 361 | # One entry per manual page. List of tuples 362 | # (source start file, name, description, authors, manual section). 363 | #man_pages = [('index', 'sfs', project, [authors], 1)] 364 | 365 | # If true, show URL addresses after external links. 366 | #man_show_urls = False 367 | 368 | 369 | # -- Options for Texinfo output ------------------------------------------- 370 | 371 | # Grouping the document tree into Texinfo files. List of tuples 372 | # (source start file, target name, title, author, 373 | # dir menu entry, description, category) 374 | #texinfo_documents = [ 375 | # ('index', 'SFS', project, project, 'SFS', 'Sound Field Synthesis Toolbox.', 376 | # 'Miscellaneous'), 377 | #] 378 | 379 | # Documents to append as an appendix to all manuals. 380 | #texinfo_appendices = [] 381 | 382 | # If false, no module index is generated. 383 | #texinfo_domain_indices = True 384 | 385 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 386 | #texinfo_show_urls = 'footnote' 387 | 388 | # If true, do not generate a @detailmenu in the "Top" node's menu. 389 | #texinfo_no_detailmenu = False 390 | 391 | 392 | # -- Options for epub output ---------------------------------------------- 393 | 394 | epub_author = authors 395 | -------------------------------------------------------------------------------- /doc/examples.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | .. only:: html 5 | 6 | You can play with the Jupyter notebooks (without having to install anything) 7 | by clicking |binder logo| on the respective example page. 8 | 9 | .. |binder logo| image:: https://mybinder.org/badge_logo.svg 10 | :target: https://mybinder.org/v2/gh/sfstoolbox/sfs-python/master? 11 | filepath=doc/examples 12 | 13 | .. toctree:: 14 | :maxdepth: 1 15 | 16 | examples/SOFA-file-access.ipynb -------------------------------------------------------------------------------- /doc/examples/MRT01.sofa: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spatialaudio/python-sofa/739d49194575aa73a075b66124949223ea87c39a/doc/examples/MRT01.sofa -------------------------------------------------------------------------------- /doc/examples/MRT01.txt: -------------------------------------------------------------------------------- 1 | Source: http://sofacoustics.org/data/database/aachen/MRT01.sofa 2 | -------------------------------------------------------------------------------- /doc/examples/MRT01_source.txt: -------------------------------------------------------------------------------- 1 | Source: http://sofacoustics.org/data/database/aachen/MRT01.sofa 2 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst 2 | 3 | ---- 4 | 5 | .. toctree:: 6 | 7 | examples 8 | api 9 | version-history 10 | 11 | .. only:: html 12 | 13 | * :ref:`genindex` 14 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx>=1.3.6 2 | Sphinx-RTD-Theme 3 | nbsphinx 4 | ipykernel 5 | sphinxcontrib-bibtex 6 | 7 | NumPy 8 | SciPy 9 | matplotlib>=1.5 10 | -------------------------------------------------------------------------------- /doc/version-history.rst: -------------------------------------------------------------------------------- 1 | .. default-role:: py:obj 2 | 3 | .. include:: ../NEWS.rst 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_file = LICENSE 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | __version__ = "unknown" 4 | 5 | # "import" __version__ 6 | for line in open("src/sofa/__init__.py"): 7 | if line.startswith("__version__"): 8 | exec(line) 9 | break 10 | 11 | setup( 12 | name="python-sofa", 13 | version=__version__, 14 | packages=find_packages('src'), 15 | package_dir={'':'src'}, 16 | install_requires=[ 17 | 'numpy', 18 | 'scipy>=1.2.0', 19 | 'netcdf4', 20 | 'datetime' 21 | ], 22 | author="Jannika Lossner", 23 | author_email="jnlossner@gmail.com", 24 | description="Python SOFA API", 25 | long_description=open('README.rst').read(), 26 | license="MIT", 27 | keywords="audio SOFA acoustics".split(), 28 | url="http://github.com/spatialaudio/python-sofa/", 29 | platforms='any', 30 | python_requires='>=3.5', 31 | classifiers=[ 32 | "Development Status :: 3 - Alpha", 33 | "License :: OSI Approved :: MIT License", 34 | "Operating System :: OS Independent", 35 | "Programming Language :: Python", 36 | "Programming Language :: Python :: 3", 37 | "Programming Language :: Python :: 3.5", 38 | "Programming Language :: Python :: 3.6", 39 | "Programming Language :: Python :: 3.7", 40 | "Programming Language :: Python :: 3 :: Only", 41 | "Topic :: Scientific/Engineering", 42 | ], 43 | zip_safe=True, 44 | ) 45 | -------------------------------------------------------------------------------- /src/sofa/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | """Python SOFA API for reading, writing and creating .sofa files. 22 | """ 23 | __version__ = "0.2.0" 24 | 25 | __all__=["access", "conventions", "datatypes", "roomtypes", "spatial", "Database"] 26 | 27 | from . import access 28 | from . import datatypes 29 | from . import spatial 30 | from . import roomtypes 31 | from . import conventions 32 | from ._database import Database 33 | 34 | ##################################### 35 | -------------------------------------------------------------------------------- /src/sofa/_database.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | """SOFA API for Python. 22 | """ 23 | __version__ = '0.1.0' 24 | 25 | from . import access 26 | from . import datatypes 27 | from . import roomtypes 28 | from . import conventions 29 | 30 | from . import spatial 31 | 32 | from enum import Enum 33 | import netCDF4 as ncdf 34 | from datetime import datetime 35 | 36 | 37 | class Database(access.ProxyObject): 38 | """Read and write NETCDF4 files following the SOFA specifications and conventions""" 39 | 40 | def __init__(self): 41 | super().__init__(self, "") 42 | 43 | self._dataset = None 44 | self._convention = None 45 | 46 | self._Dimensions = None 47 | self._Listener = None 48 | self._Source = None 49 | self._Receiver = None 50 | self._Emitter = None 51 | 52 | self._Metadata = None 53 | self._Variables = None 54 | 55 | @staticmethod 56 | def create(path, convention, dimensions=None): 57 | """Create a new .sofa file following a SOFA convention 58 | 59 | Parameters 60 | ---------- 61 | path : str 62 | Relative or absolute path to .sofa file 63 | convention : str 64 | Name of the SOFA convention to create, see :func:`sofa.conventions.implemented` 65 | dimensions : dict or int, optional 66 | Number of measurements or dict of dimensions to define (standard dimensions: "M": measurements, "R": receivers, "E": emitters, "N": data length) 67 | 68 | Returns 69 | ------- 70 | database : :class:`sofa.Database` 71 | """ 72 | sofa = Database() 73 | sofa.dataset = ncdf.Dataset(path, mode="w") 74 | if dimensions is not None: 75 | try: 76 | for d,v in dimensions.items(): 77 | sofa.Dimensions.create_dimension(d, v) 78 | except: 79 | sofa.Dimensions.create_dimension("M", dimensions) 80 | 81 | sofa._convention = conventions.List[convention]() 82 | 83 | sofa.convention.add_metadata(sofa) 84 | sofa.DateCreated = datetime.now().strftime('%Y-%m-%d %H:%M:%S') 85 | return sofa 86 | 87 | @staticmethod 88 | def open(path, mode='r', parallel=False): 89 | """Parameters 90 | ---------- 91 | path : str 92 | Relative or absolute path to .sofa file 93 | mode : str, optional 94 | File access mode ('r': readonly, 'r+': read/write) 95 | parallel : bool, optional 96 | Whether to open the file with parallel access enabled (requires parallel-enabled netCDF4) 97 | 98 | Returns 99 | ------- 100 | database : :class:`sofa.Database` 101 | """ 102 | if mode == 'w': 103 | print("Invalid file creation method, use create instead.") 104 | return None 105 | sofa = Database() 106 | sofa.dataset = ncdf.Dataset(path, mode=mode, parallel=parallel) 107 | if sofa.dataset.SOFAConventions in conventions.implemented(): 108 | sofa._convention = conventions.List[sofa.dataset.SOFAConventions]() 109 | else: 110 | default = "General" + sofa.dataset.DataType 111 | sofa._convention = conventions.List[default]() 112 | return sofa 113 | 114 | def close(self): 115 | # """Save and close the underlying NETCDF4 dataset""" 116 | try: 117 | self.save() 118 | except: 119 | pass # avoid errors when closing files in read mode 120 | if self.dataset is not None: self.dataset.close() 121 | 122 | self._dataset = None 123 | self._convention = None 124 | 125 | self._Dimensions = None 126 | self._Listener = None 127 | self._Source = None 128 | self._Receiver = None 129 | self._Emitter = None 130 | 131 | self._Metadata = None 132 | 133 | return 134 | 135 | def save(self): 136 | # """Save the underlying NETCDF4 dataset""" 137 | if self.dataset is None: return 138 | self.DateModified = datetime.now().strftime('%Y-%m-%d %H:%M:%S') 139 | self.dataset.sync() 140 | 141 | @property 142 | def convention(self): return self._convention 143 | 144 | ## data 145 | @property 146 | def Data(self): 147 | """DataType specific access for the measurement data, see :mod:`sofa.datatypes`""" 148 | return datatypes.get(self) 149 | 150 | @property 151 | def Dimensions(self): 152 | """:class:`sofa.access.Dimensions` for the database dimensions""" 153 | if self.dataset is None: 154 | print("No dataset open!") 155 | return None 156 | if self._Dimensions is None: self._Dimensions = access.Dimensions(self.dataset) 157 | return self._Dimensions 158 | 159 | ## experimental setup 160 | @property 161 | def Listener(self): 162 | """:class:`sofa.spatial.SpatialObject` for the Listener""" 163 | if self.dataset is None: 164 | print("No dataset open!") 165 | return None 166 | if self._Listener is None: self._Listener = spatial.SpatialObject(self, "Listener") 167 | return self._Listener 168 | 169 | @property 170 | def Source(self): 171 | """:class:`sofa.spatial.SpatialObject` for the Source""" 172 | if self.dataset is None: 173 | print("No dataset open!") 174 | return None 175 | if self._Source is None: self._Source = spatial.SpatialObject(self, "Source") 176 | return self._Source 177 | 178 | @property 179 | def Receiver(self): 180 | """:class:`sofa.spatial.SpatialObject` for the Receiver(s)""" 181 | if self.dataset is None: 182 | print("No dataset open!") 183 | return None 184 | if self._Receiver is None: self._Receiver = spatial.SpatialObject(self, "Receiver") 185 | return self._Receiver 186 | 187 | @property 188 | def Emitter(self): 189 | """:class:`sofa.spatial.SpatialObject` for the Emitter(s)""" 190 | if self.dataset is None: 191 | print("No dataset open!") 192 | return None 193 | if self._Emitter is None: self._Emitter = spatial.SpatialObject(self, "Emitter") 194 | return self._Emitter 195 | 196 | ## room 197 | @property 198 | def Room(self): 199 | """RoomType specific access for the room data, see :mod:`sofa.roomtypes`""" 200 | return roomtypes.get(self) 201 | 202 | ## metadata 203 | @property 204 | def Metadata(self): 205 | """:class:`sofa.access.Metadata` for the database metadata""" 206 | if self.dataset is None: 207 | print("No dataset open!") 208 | return None 209 | if self._Metadata is None: self._Metadata = access.Metadata(self.dataset) 210 | return self._Metadata 211 | 212 | ## direct access to variables 213 | @property 214 | def Variables(self): 215 | """:class:`sofa.access.DatasetVariables` for direct access to database variables""" 216 | if self.dataset is None: 217 | print("No dataset open!") 218 | return None 219 | if self._Variables is None: self._Variables = access.DatasetVariables(self) 220 | return self._Variables 221 | -------------------------------------------------------------------------------- /src/sofa/access/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | """ 22 | """ 23 | 24 | __all__=["DatasetVariables", "Dimensions", "Metadata", "ProxyObject", "StringArray", "Variable"] 25 | 26 | from .dimensions import Dimensions 27 | from .metadata import Metadata 28 | from .variables import * 29 | from .proxy import ProxyObject 30 | -------------------------------------------------------------------------------- /src/sofa/access/dimensions.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | """Class for accessing dimensions of the underlying :class:`netCDF4.Dataset`. 22 | """ 23 | 24 | class Dimensions: 25 | """Dimensions specified by SOFA as int""" 26 | 27 | def __init__(self, dataset): 28 | self.dataset = dataset 29 | return 30 | 31 | @property 32 | def C(self): 33 | """Coordinate dimension size""" 34 | return self.get_dimension("C") 35 | 36 | @property 37 | def I(self): 38 | """Scalar dimension size""" 39 | return self.get_dimension("I") 40 | 41 | @property 42 | def M(self): 43 | """Number of measurements""" 44 | return self.get_dimension("M") 45 | 46 | @property 47 | def R(self): 48 | """Number of receivers""" 49 | return self.get_dimension("R") 50 | 51 | @property 52 | def E(self): 53 | """Number of emitters""" 54 | return self.get_dimension("E") 55 | 56 | @property 57 | def N(self): 58 | """Number of data samples per measurement""" 59 | return self.get_dimension("N") 60 | 61 | @property 62 | def S(self): 63 | """Largest data string size""" 64 | return self.get_dimension("S") 65 | 66 | def get_dimension(self, dim): 67 | if dim not in self.dataset.dimensions: 68 | print("dimension {0} not initialized".format(dim)) 69 | return None 70 | return self.dataset.dimensions[dim].size 71 | 72 | def create_dimension(self, dim, size): 73 | if dim in self.dataset.dimensions: 74 | print("Dimension {0} already initialized to {1}, cannot re-initialize to {2}.".format(dim, self.get_dimension(dim), size)) 75 | return 76 | self.dataset.createDimension(dim, size) 77 | 78 | def list_dimensions(self): 79 | return self.dataset.dimensions.keys() 80 | 81 | def dump(self): 82 | """Prints all dimension sizes""" 83 | for dim in self.dataset.dimensions: 84 | print("{0}: {1}".format(dim, self.dataset.dimensions[dim].size)) 85 | return -------------------------------------------------------------------------------- /src/sofa/access/metadata.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | """Classes for accessing attribute in the underlying :class:`netCDF4.Dataset`. 22 | """ 23 | 24 | class Metadata: 25 | # """Access the dataset metadata""" 26 | def __init__(self, dataset): 27 | self.dataset = dataset 28 | 29 | def get_attribute(self, name): 30 | """Parameters 31 | ---------- 32 | name : str 33 | Name of the attribute 34 | 35 | Returns 36 | ------- 37 | value : str 38 | Value of the attribute 39 | """ 40 | if name not in self.dataset.ncattrs(): return "" 41 | return self.dataset.getncattr(name) 42 | 43 | def set_attribute(self, name, value): 44 | """Parameters 45 | ---------- 46 | name : str 47 | Name of the attribute 48 | value : str 49 | New value of the attribute 50 | """ 51 | if name not in self.dataset.ncattrs(): return self.create_attribute(name, value=value) 52 | self.dataset.setncattr(name, value) 53 | 54 | def create_attribute(self, name, value=""): 55 | """Parameters 56 | ---------- 57 | name : str 58 | Name of the attribute 59 | value : str, optional 60 | New value of the attribute 61 | """ 62 | if name in self.list_attributes(): 63 | print(name, "already in .SOFA dataset, setting value instead") 64 | self.set_attribute(name, value) 65 | return 66 | self.dataset.NewSOFAAttribute = value 67 | self.dataset.renameAttribute("NewSOFAAttribute", name) 68 | 69 | def list_attributes(self): 70 | """Returns 71 | ------- 72 | attrs : list 73 | List of the existing dataset attribute names 74 | """ 75 | return sorted(self.dataset.ncattrs()) 76 | 77 | def dump(self): 78 | """Prints all metadata attributes""" 79 | for attr in self.list_attributes(): 80 | print("{0}: {1}".format(attr, self.get_attribute(attr))) 81 | return -------------------------------------------------------------------------------- /src/sofa/access/proxy.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | """Classes for accessing arrays and data in the underlying :class:`netCDF4.Dataset`. 22 | """ 23 | 24 | class ProxyObject: 25 | """Proxy object that provides access to variables and attributes of a name group in the netCDF4 dataset""" 26 | 27 | def __init__(self, database, name): 28 | self._database = database 29 | self._name = name 30 | self.standard_dimensions = dict() 31 | return 32 | 33 | @property 34 | def database(self): 35 | return self._database 36 | 37 | @property 38 | def name(self): 39 | return self._name 40 | 41 | @property 42 | def dataset(self): 43 | try: 44 | return self._dataset 45 | except: 46 | if self.database is None: return None 47 | return self.database.dataset 48 | 49 | @dataset.setter 50 | def dataset(self, value): 51 | try: self._dataset = value 52 | except: raise 53 | 54 | @staticmethod 55 | def _valid_data_name(name): 56 | if "_" in name: return False 57 | if name in ["name", "database", "dataset", "Metadata", "Variables", "Type", "Units"]: return False 58 | return True 59 | 60 | def _get_dataset_value_or_none(self, name): 61 | if self.dataset is None: raise Exception("No dataset open!") 62 | if not ProxyObject._valid_data_name(name): raise Exception("{0} is not a valid name for a dataset value.") 63 | 64 | container_name = self.name + name 65 | if container_name in self.database.Variables.list_variables(): 66 | # attribute is a variable, return proper access class 67 | var = self.database.Variables.get_variable(container_name) 68 | if "S" not in var.dimensions(): return var 69 | else: return self.database.Variables.get_string_array(container_name) 70 | elif container_name in self.database.Metadata.list_attributes(): 71 | # attribute is an attribute in the netcdf-4 dataset 72 | return self.database.Metadata.get_attribute(container_name) 73 | else: 74 | return None 75 | 76 | def __getattribute__(self, name): 77 | try: 78 | return super().__getattribute__(name) 79 | except AttributeError: 80 | if not ProxyObject._valid_data_name(name): raise 81 | value = self._get_dataset_value_or_none(name) 82 | if value is None: 83 | print(self.name+name, "not part of .SOFA dataset") 84 | raise 85 | return value 86 | 87 | def __setattr__(self, name, value): 88 | if not ProxyObject._valid_data_name(name) or self.dataset is None: 89 | super().__setattr__(name, value) 90 | return 91 | 92 | existing = self._get_dataset_value_or_none(name) 93 | if existing is None: 94 | if type(value) == str: # attempting to set an attribute 95 | print("Adding attribute {0} to .SOFA dataset".format(self.name+name)) 96 | self.create_attribute(name, value) 97 | return 98 | raise AttributeError("{0} not part of {1}, use create_... instead.".format(name, self.name)) # we don't know what or where it should be 99 | 100 | container_name = self.name + name 101 | 102 | if type(value) == str: 103 | self.database.Metadata.set_attribute(container_name, value) 104 | return 105 | 106 | try: existing.set_values(value) 107 | except: 108 | print("Failed to set values on", container_name, "directly, use set_values instead.") 109 | raise 110 | 111 | def create_attribute(self, name, value=""): 112 | """Creates the attribute in the netCDF4 dataset with its full name self.name+name 113 | 114 | Parameters 115 | ---------- 116 | name : str 117 | Name of the variable 118 | value : str, optional 119 | Initial value of the attribute 120 | """ 121 | self.database.Metadata.create_attribute(self.name + name, value=value) 122 | 123 | def create_variable(self, name, dims, data_type="d", fill_value=0): 124 | """Creates the variable in the netCDF4 dataset with its full name self.name+name 125 | 126 | Parameters 127 | ---------- 128 | name : str 129 | Name of the variable 130 | dims : tuple(str) 131 | Dimensions of the variable 132 | 133 | Returns 134 | ------- 135 | value : `sofa.access.Variable` 136 | Access object for the variable 137 | """ 138 | if name in self.standard_dimensions: 139 | std_dims = self.standard_dimensions[name] 140 | if dims not in std_dims: raise ValueError("Dimensions {0} not standard: {1}".format(dims, std_dims)) 141 | return self.database.Variables.create_variable(self.name + name, dims, data_type=data_type, 142 | fill_value=fill_value) 143 | 144 | def create_string_array(self, name, dims): 145 | """Creates the string array in the netCDF4 dataset with its full name self.name+name 146 | 147 | Parameters 148 | ---------- 149 | name : str 150 | Name of the variable 151 | dims : tuple(str) 152 | Dimensions of the variable 153 | 154 | Returns 155 | ------- 156 | value : `sofa.access.StringArray` 157 | Access object for the string array 158 | """ 159 | if name in self.standard_dimensions: 160 | std_dims = self.standard_dimensions[name] 161 | if dims not in std_dims: raise ValueError("Dimensions {0} not standard: {1}".format(dims, std_dims)) 162 | return self.database.Variables.create_string_array(self.name + name, dims) 163 | 164 | # @property 165 | # def Description(self): 166 | # """Informal description of the object""" 167 | # return self.database.__getattribute__(self.name + "Description") 168 | 169 | # @Description.setter 170 | # def Description(self, value): 171 | # self.database.__setattr__(self.name + "Description", value) 172 | -------------------------------------------------------------------------------- /src/sofa/access/variables.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | """Classes for accessing arrays and data in the underlying :class:`netCDF4.Dataset`. 22 | """ 23 | 24 | #__all__ = ["get_values_from_array", "DatasetVariables", "StringArray", "Variable"] 25 | 26 | import numpy as np 27 | 28 | 29 | def filled_if_masked(array): 30 | if type(array) is np.ma.MaskedArray: return array.filled() 31 | return array 32 | 33 | 34 | def is_integer(val): 35 | return np.issubdtype(type(val), np.integer) 36 | 37 | 38 | def get_slice_tuple(dimensions, indices=None): 39 | if indices is None: return tuple([slice(None) for x in dimensions]) 40 | if "M" in indices and "I" in dimensions: 41 | indices["I"] = 0 if is_integer(indices["M"]) else slice(None) 42 | return tuple([slice(None) if x not in indices else indices[x] for x in dimensions]) 43 | 44 | 45 | def get_default_dimension_order(dimensions, indices=None): 46 | if indices is None: return dimensions 47 | if "M" in indices and "I" in dimensions: 48 | indices["I"] = 0 if is_integer(indices["M"]) else slice(None) 49 | dim_order = tuple([x for x in dimensions if x not in indices or not is_integer(indices[x])]) 50 | return dim_order 51 | 52 | 53 | def get_dimension_order_transposition(original, new): 54 | old = original 55 | if "M" in new and "I" in old: # replace "I" with "M" if necessary 56 | old = list(old) 57 | old[old.index("I")] = "M" 58 | if "M" in old and "I" in new: # replace "I" with "M" if necessary 59 | new = list(new) 60 | new[new.index("I")] = "M" 61 | transposition = [old.index(x) for x in new] 62 | return tuple(transposition) 63 | 64 | 65 | def get_values_from_array(array, dimensions, indices=None, dim_order=None): 66 | """Extract values of a given range from an array 67 | 68 | Parameters 69 | ---------- 70 | array : array_like 71 | Source array 72 | dimensions : tuple of str 73 | Names of the array dimensions in order 74 | indices : dict(key:str, value:int or slice), optional 75 | Key: dimension name, value: indices to be returned, complete axis assumed if not provided 76 | dim_order : tuple of str 77 | Desired order of dimensions in the output array 78 | 79 | Returns 80 | ------- 81 | values : np.ndarray 82 | Requested array range in regular or desired dimension order, if provided 83 | """ 84 | sls = get_slice_tuple(dimensions, indices) 85 | if dim_order is None: return filled_if_masked(array[sls]) 86 | 87 | old_dim_order = get_default_dimension_order(dimensions, indices) 88 | transposition = get_dimension_order_transposition(old_dim_order, dim_order) 89 | 90 | try: 91 | return filled_if_masked(np.transpose(array[sls], transposition)) 92 | except Exception as e: 93 | raise Exception( 94 | "dimension mismatch: cannot transpose from {0} to {1} in order {2}, error {3}".format(old_dim_order, 95 | dim_order, 96 | transposition, e)) 97 | return transposed 98 | 99 | class _VariableBase: 100 | # """Access the values of a NETCDF4 dataset variable""" 101 | def __init__(self, database, name): 102 | # """ 103 | # Parameters 104 | # ---------- 105 | # database : :class:`sofa.Database` 106 | # Parent database instance 107 | # name : str 108 | # Variable name within the netCDF4 dataset 109 | # """ 110 | self._database = database 111 | self._name = name 112 | 113 | @property 114 | def name(self): 115 | return self._name 116 | @property 117 | def database(self): 118 | return self._database 119 | 120 | def __getattribute__(self, name): 121 | try: 122 | return super().__getattribute__(name) 123 | except AttributeError: 124 | try: 125 | return self._Matrix.__getattribute__(name) 126 | except: raise 127 | 128 | def __setattr__(self, name, value): 129 | if '_' in name: 130 | super().__setattr__(name, value) 131 | return 132 | 133 | # TODO: are there any cases in which this is wrong? 134 | self._Matrix.setncattr_string(name, value) 135 | 136 | def initialize(self, dims, data_type="d", fill_value=0): 137 | """Create the variable in the underlying netCDF4 dataset""" 138 | defined = self.database.Dimensions.list_dimensions() 139 | missing = [] 140 | for d in dims: 141 | if d not in defined: missing.append(d) 142 | if len(missing): raise Exception("Cannot initialize, dimensions undefined: {0}".format(missing)) 143 | try: 144 | self.database.dataset.createVariable(self.name, data_type, dims, fill_value=fill_value) 145 | except Exception as ex: 146 | raise Exception( 147 | "Failed to create variable for {0} of type {1} with fill value {2}, error = {3}".format(self.name, 148 | data_type, dims, 149 | fill_value, 150 | str(ex))) 151 | 152 | @property 153 | def _Matrix(self): 154 | if self.name not in self.database.Variables.list_variables(): return None 155 | return self.database.dataset[self.name] 156 | 157 | def exists(self): 158 | """Returns 159 | ------- 160 | exists : bool 161 | True if variable exists, False otherwise 162 | """ 163 | return self._Matrix is not None 164 | 165 | def dimensions(self): 166 | """Returns 167 | ------- 168 | dimensions : tuple of str 169 | Variable dimension names in order 170 | """ 171 | if not self.exists(): return None 172 | return self._Matrix.dimensions 173 | 174 | def axis(self, dim): 175 | """Parameters 176 | ---------- 177 | dim : str 178 | Name of the dimension 179 | 180 | Returns 181 | ------- 182 | axis : int 183 | Index of the dimension axis or None if unused 184 | """ 185 | if dim in self.dimensions(): return self.dimensions().index(dim) 186 | if dim == "M" and "I" in self.dimensions(): return self.dimensions().index("I") 187 | return None 188 | 189 | def get_values(self, indices=None, dim_order=None): 190 | """ 191 | Parameters 192 | ---------- 193 | indices : dict(key:str, value:int or slice), optional 194 | Key: dimension name, value: indices to be returned, complete axis assumed if not provided 195 | dim_order : tuple of str, optional 196 | Desired order of dimensions in the output array 197 | 198 | Returns 199 | ------- 200 | values : np.ndarray 201 | Requested array range in regular or desired dimension order, if provided 202 | """ 203 | if not self.exists(): 204 | raise Exception("failed to get values of {0}, variable not initialized".format(self.name)) 205 | return get_values_from_array(self._Matrix, self.dimensions(), indices=indices, dim_order=dim_order) 206 | 207 | def _reorder_values_for_set(self, values, indices=None, dim_order=None, repeat_dim=None): 208 | """ 209 | Parameters 210 | ---------- 211 | values : np.ndarray 212 | New values for the array range 213 | indices : dict(key:str, value:int or slice), optional 214 | Key: dimension name, value: indices to be set, complete axis assumed if not provided 215 | dim_order : tuple of str, optional 216 | Dimension names in provided order, regular order assumed 217 | repeat_dim : tuple of str, optional 218 | Tuple of dimension names along which to repeat the values 219 | """ 220 | if not self.exists(): 221 | raise Exception("Variable {0} not initialized".format(self.name)) 222 | dimensions = self.dimensions() 223 | if "I" in dimensions: 224 | dimensions = list(dimensions) 225 | dimensions[dimensions.index("I")] = "M" 226 | dimensions = tuple(dimensions) 227 | 228 | if indices is not None and "M" in indices.keys(): indices["M"] = 0 229 | 230 | if dim_order is not None and "I" in dim_order: 231 | dim_order = list(dim_order) 232 | dim_order[dim_order.index("I")] = "M" 233 | dim_order = tuple(dim_order) 234 | if repeat_dim is not None and "I" in repeat_dim: 235 | repeat_dim = list(repeat_dim) 236 | repeat_dim[repeat_dim.index("I")] = "M" 237 | repeat_dim = tuple(repeat_dim) 238 | 239 | sls = () 240 | for d in dimensions: 241 | sl = slice(None) 242 | if indices is not None and d in indices: sl = indices[d] 243 | sls = sls + (sl,) 244 | new_values = np.asarray(values) 245 | 246 | # repeat along provided dimensions 247 | full_dim_order = dim_order 248 | if repeat_dim is not None: 249 | if full_dim_order is None: 250 | full_dim_order = tuple(x for x in dimensions if x not in repeat_dim) 251 | for d in repeat_dim: 252 | if dim_order is not None and d in dim_order: 253 | raise Exception("cannot repeat values along dimension {0}: dimension already provided".format(d)) 254 | return None 255 | i = self.axis(d) 256 | if i is None: 257 | raise Exception( 258 | "cannot repeat values along dimension {0}: dimension unused by variable {1}".format(d, 259 | self.name)) 260 | return None 261 | count = self._Matrix[sls].shape[i] 262 | new_values = np.repeat([new_values], count, axis=0) 263 | full_dim_order = (d,) + full_dim_order 264 | 265 | # change order if necessary 266 | if full_dim_order is not None: 267 | do = () 268 | for d in dimensions: 269 | if d in full_dim_order: 270 | if indices is not None and d in indices.keys() and type(indices[d]) != slice: 271 | raise Exception( 272 | "cannot assign values to variable {0}: dimension {1} is {2}, not a slice".format(self.name, 273 | d, type( 274 | indices[d]))) 275 | return None 276 | do = do + (full_dim_order.index(d),) 277 | elif indices is None or d not in indices.keys(): 278 | raise Exception("cannot assign values to variable {0}: missing dimension {1}".format(self.name, d)) 279 | return None 280 | new_values = np.transpose(new_values, do) 281 | 282 | return new_values, sls 283 | 284 | def set_values(self, values, indices=None, dim_order=None, repeat_dim=None): 285 | """ 286 | Parameters 287 | ---------- 288 | values : np.ndarray 289 | New values for the array range 290 | indices : dict(key:str, value:int or slice), optional 291 | Key: dimension name, value: indices to be set, complete axis assumed if not provided 292 | dim_order : tuple of str, optional 293 | Dimension names in provided order, regular order assumed 294 | repeat_dim : tuple of str, optional 295 | Tuple of dimension names along which to repeat the values 296 | """ 297 | if not self.exists(): 298 | raise Exception("failed to set values of {0}, variable not initialized".format(self.name)) 299 | new_values, sls = self._reorder_values_for_set(values, indices, dim_order, repeat_dim) 300 | 301 | # assign 302 | self._Matrix[sls] = new_values 303 | return 304 | 305 | class Variable(_VariableBase): 306 | def __init__(self, database, name): 307 | super().__init__(database, name) 308 | self._unit_proxy = None 309 | 310 | @property 311 | def Units(self): 312 | """Units of the values""" 313 | if not self.exists(): 314 | raise Exception("failed to get Units of {0}, variable not initialized".format(self.name)) 315 | if self._unit_proxy is None: return self._Matrix.Units 316 | return self._unit_proxy.Units 317 | @Units.setter 318 | def Units(self, value): 319 | if not self.exists(): 320 | raise Exception("failed to set Units of {0}, variable not initialized".format(self.name)) 321 | self._Matrix.Units = value 322 | 323 | class DatasetVariables: 324 | # """Direct access the dataset variables""" 325 | def __init__(self, database): 326 | self.database = database 327 | 328 | def get_variable(self, name): 329 | """Parameters 330 | ---------- 331 | name : str 332 | Name of the variable 333 | 334 | Returns 335 | ------- 336 | value : `sofa.access.Variable` 337 | Access object for the variable 338 | """ 339 | return Variable(self.database, name) 340 | 341 | def get_string_array(self, name): 342 | """Parameters 343 | ---------- 344 | name : str 345 | Name of the string array 346 | 347 | Returns 348 | ------- 349 | value : `sofa.access.StringArray` 350 | Access object for the string array 351 | """ 352 | return StringArray(self.database, name) 353 | 354 | def create_variable(self, name, dims, data_type="d", fill_value=0): 355 | """Parameters 356 | ---------- 357 | name : str 358 | Name of the variable 359 | dims : tuple(str) 360 | Dimensions of the variable 361 | 362 | Returns 363 | ------- 364 | value : `sofa.access.Variable` 365 | Access object for the variable 366 | """ 367 | var = self.get_variable(name) 368 | if var.exists(): 369 | # TODO: add raise error? 370 | print(name, "already exists in the dataset!") 371 | return var 372 | var.initialize(dims, data_type=data_type, fill_value=fill_value) 373 | return var 374 | 375 | def create_string_array(self, name, dims): 376 | """Parameters 377 | ---------- 378 | name : str 379 | Name of the variable 380 | dims : tuple(str) 381 | Dimensions of the variable 382 | 383 | Returns 384 | ------- 385 | value : `sofa.access.StringArray` 386 | Access object for the string array 387 | """ 388 | var = self.get_string_array(name) 389 | if var.exists(): 390 | # TODO: add raise error? 391 | print(name, "already exists in the dataset!") 392 | return var 393 | var.initialize(dims) 394 | return var 395 | 396 | def list_variables(self): 397 | """Returns 398 | ------- 399 | attrs : list 400 | List of the existing dataset variable and string array names 401 | """ 402 | return sorted(self.database.dataset.variables.keys()) 403 | 404 | def dump(self): 405 | """Prints all variables and their dimensions""" 406 | for vname in self.list_variables(): 407 | print("{0}: {1}".format(vname, self.get_variable(vname).dimensions())) 408 | return 409 | 410 | class StringArray(_VariableBase): 411 | def initialize(self, dims, data_type="c", fill_value='\0'): 412 | """Create the zero-padded character array in the underlying netCDF4 dataset. 413 | Dimension 'S' must be the last dimension, and is appended if not included in dims.""" 414 | if "S" not in dims: dims = dims + ("S",) 415 | if dims[-1] != "S": raise Exception("Failed to initialize character array with dimensions {0}, 'S' must be last dimension.".format(dims)) 416 | super().initialize(dims, data_type, fill_value) 417 | 418 | def get_values(self, indices=None, dim_order=None): 419 | """ 420 | Parameters 421 | ---------- 422 | indices : dict(key:str, value:int or slice), optional 423 | Key: dimension name, value: indices to be returned, complete axis assumed if not provided 424 | dim_order : tuple of str, optional 425 | Desired order of dimensions in the output array 426 | 427 | Returns 428 | ------- 429 | values : np.ndarray 430 | Requested array range in regular or desired dimension order, if provided 431 | """ 432 | if dim_order is not None and "S" not in dim_order: dim_order = dim_order + ("S",) 433 | return super().get_values(indices, dim_order) 434 | 435 | def set_values(self, values, indices=None, dim_order=None, repeat_dim=None): 436 | """ 437 | Parameters 438 | ---------- 439 | values : np.ndarray 440 | New values for the array range 441 | indices : dict(key:str, value:int or slice), optional 442 | Key: dimension name, value: indices to be set, complete axis assumed if not provided 443 | dim_order : tuple of str, optional 444 | Dimension names in provided order, regular order assumed 445 | repeat_dim : tuple of str, optional 446 | Tuple of dimension names along which to repeat the values 447 | """ 448 | if dim_order is not None and "S" not in dim_order: dim_order = dim_order + ("S",) 449 | # TODO: accept nested lists of strings that may be too short, convert into proper character array 450 | return super().set_values(values, indices, dim_order, repeat_dim) -------------------------------------------------------------------------------- /src/sofa/conventions/GeneralFIR.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from .base import _Base 22 | 23 | from .. import spatial 24 | 25 | class GeneralFIR(_Base): 26 | name = "GeneralFIR" 27 | version = "1.0" 28 | 29 | def add_metadata(self, database): 30 | super().add_metadata(database) 31 | 32 | database.Metadata.set_attribute("SOFAConventions", self.name) 33 | database.Metadata.set_attribute("SOFAConventionsVersion", self.version) 34 | 35 | database.Data.Type = "FIR" 36 | return 37 | 38 | -------------------------------------------------------------------------------- /src/sofa/conventions/GeneralFIRE.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from .base import _Base 22 | 23 | from .. import spatial 24 | 25 | class GeneralFIRE(_Base): 26 | name = "GeneralFIRE" 27 | version = "1.0" 28 | 29 | def add_metadata(self, database): 30 | super().add_metadata(database) 31 | 32 | database.Metadata.set_attribute("SOFAConventions", self.name) 33 | database.Metadata.set_attribute("SOFAConventionsVersion", self.version) 34 | 35 | database.Data.Type = "FIRE" 36 | return 37 | -------------------------------------------------------------------------------- /src/sofa/conventions/GeneralTF.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from .base import _Base 22 | 23 | from .. import spatial 24 | 25 | class GeneralTF(_Base): 26 | name = "GeneralTF" 27 | version = "1.0" 28 | 29 | def add_metadata(self, database): 30 | super().add_metadata(database) 31 | 32 | database.Metadata.set_attribute("SOFAConventions", self.name) 33 | database.Metadata.set_attribute("SOFAConventionsVersion", self.version) 34 | 35 | database.Data.Type = "FIR" 36 | return 37 | -------------------------------------------------------------------------------- /src/sofa/conventions/MultiSpeakerBRIR.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from .SimpleFreeFieldHRIR import SimpleFreeFieldHRIR 22 | 23 | class MultiSpeakerBRIR(SimpleFreeFieldHRIR): 24 | name = "MultiSpeakerBRIR" 25 | version = "0.3" 26 | def __init__(self): 27 | super().__init__() 28 | self.default_objects["Receiver"]["count"] = 2 29 | 30 | #self.default_data["IR"] = 1 31 | 32 | self.conditions["must have 2 Receivers"] = lambda name, fixed, variances, count: name != "Receiver" or count == 2 33 | self.conditions["must have Listener Up and View"] = lambda name, fixed, variances, count: name != "Listener" or ("Up" in fixed + variances and "View" in fixed + variances) 34 | self.conditions["must have both Emitter View and Up or neither"] = lambda name, fixed, variances, count: name != "Emitter" or "View" not in fixed + variances or ("Up" in fixed + variances and "View" in fixed + variances) 35 | 36 | def add_metadata(self, database): 37 | super().add_metadata(database) 38 | 39 | database.Data.Type = "FIRE" 40 | database.Room.Type = "reverberant" 41 | return 42 | -------------------------------------------------------------------------------- /src/sofa/conventions/SimpleFreeFieldHRIR.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from .base import _Base 22 | 23 | from .. import spatial 24 | 25 | class SimpleFreeFieldHRIR(_Base): 26 | name = "SimpleFreeFieldHRIR" 27 | version = "1.0" 28 | def __init__(self): 29 | _Base.__init__(self) 30 | self.default_objects["Emitter"]["count"] = 1 31 | #self.default_objects["Receiver"]["count"] = 2 # note: standardized convention allows unlimited receivers, despite the name HRIR 32 | 33 | self.head_radius = 0.09 34 | 35 | self.conditions["must have 1 Emitter"] = lambda name, fixed, variances, count: name != "Emitter" or count == 1 36 | #self.conditions["must have 2 Receivers"] = lambda name, fixed, variances, count: name != "Receiver" or count == 2 37 | self.conditions["must have Listener Up and View"] = lambda name, fixed, variances, count: name != "Listener" or ("Up" in fixed + variances and "View" in fixed + variances) 38 | 39 | def add_metadata(self, database): 40 | super().add_metadata(database) 41 | 42 | database.Metadata.set_attribute("SOFAConventions", self.name) 43 | database.Metadata.set_attribute("SOFAConventionsVersion", self.version) 44 | 45 | database.Data.Type = "FIR" 46 | 47 | database.Metadata.set_attribute("DatabaseName", "") 48 | database.Metadata.set_attribute("ListenerShortName", "") 49 | return 50 | 51 | def set_default_spatial_values(self, spobj): 52 | super().set_default_spatial_values(spobj) 53 | 54 | self.set_default_Receiver(spobj) 55 | return 56 | 57 | def set_default_Receiver(self, spobj): 58 | if spobj.name != "Receiver": return 59 | if spobj.database.Dimensions.R == 2: 60 | spobj.Position.set_values([[0,self.head_radius,0], [0,-self.head_radius,0]], dim_order=("R", "C"), repeat_dim=("M")) 61 | -------------------------------------------------------------------------------- /src/sofa/conventions/SimpleFreeFieldSOS.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from .SimpleFreeFieldHRIR import SimpleFreeFieldHRIR 22 | 23 | class SimpleFreeFieldSOS(SimpleFreeFieldHRIR): 24 | name = "SimpleFreeFieldSOS" 25 | version = "1.0" 26 | 27 | def add_metadata(self, database): 28 | super().add_metadata(database) 29 | 30 | database.Data.Type = "SOS" 31 | return 32 | -------------------------------------------------------------------------------- /src/sofa/conventions/SimpleFreeFieldTF.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from .SimpleFreeFieldHRIR import SimpleFreeFieldHRIR 22 | 23 | class SimpleFreeFieldTF(SimpleFreeFieldHRIR): 24 | name = "SimpleFreeFieldTF" 25 | version = "1.0" 26 | 27 | def add_metadata(self, database): 28 | super().add_metadata(database) 29 | 30 | database.Data.Type = "TF" 31 | return 32 | -------------------------------------------------------------------------------- /src/sofa/conventions/SimpleHeadphoneIR.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from .SimpleFreeFieldHRIR import SimpleFreeFieldHRIR 22 | 23 | ### incomplete! Requires extending the data access to include per-measurement strings 24 | class SimpleHeadphoneIR(SimpleFreeFieldHRIR): 25 | name = "SimpleHeadphoneIR" 26 | version = "0.2" 27 | def __init__(self): 28 | super().__init__() 29 | self.default_objects["Emitter"]["count"] = 2 30 | self.default_objects["Receiver"]["count"] = 2 31 | 32 | self.conditions["must have 2 Emitters"] = lambda name, info_states, count: name != "Emitter" or count == 2 33 | self.conditions["must have 2 Receivers"] = lambda name, info_states, count: name != "Receiver" or count == 2 34 | 35 | def add_metadata(self, database): 36 | super().add_metadata(database) 37 | 38 | return 39 | 40 | def set_default_spatial_values(self, spobj): 41 | super.set_default_spatial_values(spobj) 42 | 43 | self.set_default_Emitter(spobj) 44 | return 45 | 46 | def set_default_Emitter(self, spobj): 47 | if spobj.name != "Emitter": return 48 | spobj.Position.set_values([[0,self.head_radius,0], [0,-self.head_radius,0]], dim_order=("E", "C"), repeat_dim=("M")) -------------------------------------------------------------------------------- /src/sofa/conventions/SingleRoomDRIR.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from .base import _Base 22 | 23 | from .. import spatial 24 | 25 | class SingleRoomDRIR(_Base): 26 | name = "SingleRoomDRIR" 27 | version = "0.3" 28 | 29 | def __init__(self): 30 | _Base.__init__(self) 31 | self.default_objects["Source"]["coordinates"]["View"] = [-1,0,0] 32 | self.default_objects["Emitter"]["count"] = 1 33 | 34 | self.conditions["must have 1 Emitter"] = lambda name, fixed, variances, count: name != "Emitter" or count == 1 35 | self.conditions["must have Listener Up and View"] = lambda name, fixed, variances, count: name != "Listener" or ("Up" in fixed + variances and "View" in fixed + variances) 36 | self.conditions["must have Source Up and View"] = lambda name, fixed, variances, count: name != "Source" or ("Up" in fixed + variances and "View" in fixed + variances) 37 | 38 | def add_metadata(self, database): 39 | super().add_metadata(database) 40 | 41 | database.Metadata.set_attribute("SOFAConventions", self.name) 42 | database.Metadata.set_attribute("SOFAConventionsVersion", self.version) 43 | 44 | database.Data.Type = "FIR" 45 | database.Room.Type = "reverberant" 46 | return 47 | -------------------------------------------------------------------------------- /src/sofa/conventions/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | """ 22 | """ 23 | 24 | __all__=["implemented"] 25 | 26 | from . import base 27 | 28 | from . import GeneralFIR 29 | from . import GeneralTF 30 | from . import SimpleFreeFieldHRIR 31 | 32 | from . import GeneralFIRE 33 | from . import MultiSpeakerBRIR 34 | from . import SimpleFreeFieldTF 35 | from . import SimpleFreeFieldSOS 36 | #from . import SimpleHeadphoneIR 37 | from . import SingleRoomDRIR 38 | 39 | List = { 40 | "GeneralFIR" : GeneralFIR.GeneralFIR, 41 | "GeneralTF" : GeneralTF.GeneralTF, 42 | "SimpleFreeFieldHRIR" : SimpleFreeFieldHRIR.SimpleFreeFieldHRIR, 43 | 44 | "GeneralFIRE" : GeneralFIRE.GeneralFIRE, 45 | "MultiSpeakerBRIR" : MultiSpeakerBRIR.MultiSpeakerBRIR, 46 | "SimpleFreeFieldTF" : SimpleFreeFieldTF.SimpleFreeFieldTF, 47 | "SimpleFreeFieldSOS" : SimpleFreeFieldSOS.SimpleFreeFieldSOS, 48 | # "SimpleHeadphoneIR" : SimpleHeadphoneIR.SimpleHeadphoneIR, 49 | "SingleRoomDRIR" : SingleRoomDRIR.SingleRoomDRIR 50 | } 51 | 52 | def implemented(): 53 | """Returns 54 | ------- 55 | list 56 | Names of implemented SOFA conventions 57 | """ 58 | #TODO: versionize convention implementations 59 | return list(List.keys()) 60 | -------------------------------------------------------------------------------- /src/sofa/conventions/base.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from .. import datatypes 22 | from .. import spatial 23 | 24 | class _Base: 25 | def __init__(self): 26 | self.default_objects = { 27 | "Listener" : { 28 | "count" : 1, 29 | "coordinates" : {}, 30 | "system" : {} 31 | }, 32 | "Receiver" : { 33 | "coordinates": {}, 34 | "system": {} 35 | }, 36 | "Source" : { 37 | "count" : 1, 38 | "coordinates" : {}, 39 | "system" : {} 40 | }, 41 | "Emitter" : { 42 | "coordinates": {}, 43 | "system": {} 44 | }, 45 | } 46 | self.conditions = { 47 | "only 1 Listener considered" : lambda name, fixed, variances, count: name != "Listener" or count == 1, 48 | "only 1 Source considered" : lambda name, fixed, variances, count: name != "Source" or count == 1, 49 | } 50 | self.default_data = { 51 | "IR" : 0, 52 | "Delay" : 0, 53 | "SamplingRate" : 48000, 54 | "SamplingRate:Units" : "hertz", 55 | 56 | "Real" : 0, 57 | "Imag" : 0, 58 | "N" : 0, 59 | "N.LongName" : "frequency", 60 | 61 | "SOS" : 0 #TODO repeat [0 0 0 1 0 0] along N 62 | } 63 | return 64 | 65 | @staticmethod 66 | def add_general_defaults(database): 67 | database.Metadata.set_attribute("Conventions", "SOFA") 68 | database.Metadata.set_attribute("Version", "1.0") 69 | database.Metadata.set_attribute("Title", "") 70 | database.Metadata.set_attribute("DateCreated", "") 71 | database.Metadata.set_attribute("DateModified", "") 72 | database.Metadata.set_attribute("APIName", "python-SOFA") 73 | database.Metadata.set_attribute("APIVersion", "0.2") 74 | database.Metadata.set_attribute("AuthorContact", "") 75 | database.Metadata.set_attribute("Organization", "") 76 | database.Metadata.set_attribute("License", "No license provided, ask the author for permission") 77 | 78 | database.Metadata.set_attribute("RoomType", "free field") 79 | database.Metadata.set_attribute("DataType", "FIR") 80 | 81 | database.Dimensions.create_dimension("I", 1) 82 | database.Dimensions.create_dimension("C", 3) 83 | return 84 | 85 | def add_metadata(self, database): 86 | _Base.add_general_defaults(database) 87 | 88 | def validate_spatial_object_settings(self, name, fixed, variances, count): 89 | for con in self.conditions: 90 | if not self.conditions[con](name, fixed, variances, count): raise Exception(con) 91 | 92 | def set_default_spatial_values(self, spobj): 93 | name = spobj.name 94 | if name not in self.default_objects: return 95 | coordinates = self.default_objects[name]["coordinates"] 96 | system = self.default_objects[name]["system"] 97 | 98 | if spobj.Position.exists(): 99 | rd = tuple(x for x in spobj.Position.dimensions() if x != "C") 100 | if "Position" in system: spobj.Position.set_system(csystem=system) 101 | if "Position" in coordinates: spobj.Position.set_values(coordinates["Position"], repeat_dim = rd) 102 | if spobj.View.exists(): 103 | rd = tuple(x for x in spobj.View.dimensions() if x != "C") 104 | if "View" in system: spobj.View.set_system(csystem=system) 105 | if "View" in coordinates: spobj.View.set_values(coordinates["View"], repeat_dim = rd) 106 | if spobj.Up.exists(): 107 | rd = tuple(x for x in spobj.Up.dimensions() if x != "C") 108 | if "Up" in system: spobj.Up.set_system(csystem=system) 109 | if "Up" in coordinates: spobj.View.set_values(coordinates["Up"], repeat_dim = rd) 110 | return 111 | 112 | 113 | -------------------------------------------------------------------------------- /src/sofa/datatypes/FIR.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from .base import _Base 22 | 23 | class FIR(_Base): 24 | """Finite Impulse Response data type 25 | 26 | IR : `sofa.access.Variable` 27 | Discrete time impulse responses, dimensions ('M', 'R', 'N') 28 | Delay : `sofa.access.Variable` 29 | Broadband delay in units of dimension 'N', dimensions ('I', 'R') or ('M', 'R') 30 | SamplingRate : `sofa.access.Variable` 31 | Sampling rate, dimensions ('I') or ('M'), with attribute "Units" 32 | """ 33 | 34 | def __init__(self, database): 35 | super().__init__(database) 36 | self.standard_dimensions["IR"] = [("M", "R", "N")] 37 | self.standard_dimensions["Delay"] = [("I", "R"), ("M", "R")] 38 | self.standard_dimensions["SamplingRate"] = [("I",), ("M",)] 39 | 40 | -------------------------------------------------------------------------------- /src/sofa/datatypes/FIRE.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from .base import _Base 22 | 23 | class FIRE(_Base): 24 | """Finite Impulse Response per Emitter data type 25 | 26 | IR : `sofa.access.Variable` 27 | Discrete time impulse responses, dimensions ('M', 'R', 'E', 'N') 28 | Delay : `sofa.access.Variable` 29 | Broadband delay in units of dimension 'N', dimensions ('I', 'R', 'E') or ('M', 'R', 'E') 30 | SamplingRate : `sofa.access.Variable` 31 | Sampling rate, dimensions ('I') or ('M'), with attribute "Units" 32 | """ 33 | 34 | def __init__(self, database): 35 | super().__init__(database) 36 | self.standard_dimensions["IR"] = [("M", "R", "E", "N")] 37 | self.standard_dimensions["Delay"] = [("I", "R", "E"), ("M", "R", "E")] 38 | self.standard_dimensions["SamplingRate"] = [("I",), ("M",)] 39 | -------------------------------------------------------------------------------- /src/sofa/datatypes/SOS.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from .base import _Base 22 | 23 | 24 | class SOS(_Base): 25 | """Second Order Sections data type 26 | 27 | SOS : `sofa.access.Variable` 28 | Second order sections, dimensions ('M', 'R', 'N') 29 | Delay : `sofa.access.Variable` 30 | Broadband delay in samples resulting from SamplingRate, dimensions ('M', 'R') 31 | SamplingRate : `sofa.access.Variable` 32 | Sampling rate, dimensions ('I') or ('M'), with attribute "Units" 33 | """ 34 | 35 | def __init__(self, database): 36 | super().__init__(database) 37 | self.standard_dimensions["IR"] = [("M", "R", "N")] 38 | self.standard_dimensions["Delay"] = [("M", "R")] 39 | self.standard_dimensions["SamplingRate"] = [("I",), ("M",)] 40 | 41 | def initialize(self, sample_count=None, variances=[], string_length=None): 42 | if sample_count % 6 != 0: raise Exception( 43 | "Cannot initialize SOS DataType with dimension 'N'={0}, must be multiple of 6!".format(sample_count)) 44 | super().initialize(sample_count, variances, string_length) 45 | -------------------------------------------------------------------------------- /src/sofa/datatypes/TF.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from .base import _Base 22 | 23 | 24 | class TF(_Base): 25 | """Transfer Function data type 26 | 27 | Real : `sofa.access.Variable` 28 | Real part of the complex spectrum, dimensions ('M', 'R', 'N') 29 | Imag : `sofa.access.Variable` 30 | Imaginary part of the complex spectrum, dimensions ('M', 'R', 'N') 31 | N : `sofa.access.Variable` 32 | Frequency values, dimension ('N',), with attributes "LongName" and "Units" 33 | """ 34 | 35 | def __init__(self, database): 36 | super().__init__(database) 37 | self.standard_dimensions["Real"] = [("M", "R", "N")] 38 | self.standard_dimensions["Imag"] = [("M", "R", "N")] 39 | 40 | @property 41 | def N(self): 42 | """Frequency values""" 43 | return self.database.Variables.get_variable("N") 44 | 45 | @N.setter 46 | def N(self, value): self.N.set_values(value) 47 | 48 | def initialize(self, sample_count=None, variances=[], string_length=None): 49 | super().initialize(sample_count, variances, string_length) 50 | var = self.database.Variables.create_variable("N", ("N",)) 51 | # var.LongName = "frequency" # LongName not mandatory 52 | var.Units = "hertz" 53 | -------------------------------------------------------------------------------- /src/sofa/datatypes/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | """Classes for accessing DataType-specific measurement data. 22 | """ 23 | 24 | __all__=["implemented", "FIR", "FIRE", "SOS", "TF"] 25 | 26 | from .FIR import FIR 27 | from .TF import TF 28 | 29 | from .FIRE import FIRE 30 | from .SOS import SOS 31 | 32 | ############################## 33 | List = { 34 | "FIR" : FIR, 35 | "TF" : TF, 36 | "FIRE" : FIRE, 37 | "SOS" : SOS 38 | } 39 | 40 | def get(database): 41 | if database.DataType in List.keys(): return List[database.dataset.DataType](database) 42 | print("Unknown DataType", database.DataType, ", returning FIR instead") 43 | return List["FIR"](database) 44 | 45 | def implemented(): 46 | """Returns 47 | ------- 48 | list 49 | Names of implemented SOFA data types 50 | """ 51 | return list(List.keys()) 52 | -------------------------------------------------------------------------------- /src/sofa/datatypes/base.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from .. import access 22 | 23 | class _Base(access.ProxyObject): 24 | def __init__(self, database): 25 | super().__init__(database, "Data.") 26 | 27 | @property 28 | def Type(self): 29 | """SOFA data type""" 30 | return self.database.Metadata.get_attribute("DataType") 31 | @Type.setter 32 | def Type(self, value): self.database.Metadata.set_attribute("DataType", value) 33 | 34 | def optional_variance_names(self): 35 | """Returns a list of standardized data elements that may vary between measurements""" 36 | vardims = [] 37 | for k, v in self.standard_dimensions: 38 | if any(["I" in dims for dims in v]) and any(["M" in dims for dims in v]): vardims.append(k) 39 | return vardims 40 | 41 | def initialize(self, sample_count=None, variances=[], string_length=None): 42 | """Create the necessary variables and attributes 43 | 44 | Parameters 45 | ---------- 46 | sample_count : int, optional 47 | Number of samples per measurement, mandatory if dimension N has not been defined 48 | variances : list 49 | Names of the variables that vary along dimension M 50 | string_length : int, optional 51 | Size of the longest data string 52 | """ 53 | if "N" not in self.database.Dimensions.list_dimensions(): 54 | if sample_count is None: raise ValueError("Missing sample count N!") 55 | self.database.Dimensions.create_dimension("N", sample_count) 56 | if string_length is not None: self.database.Dimensions.create_dimension("S", string_length) 57 | 58 | default_values = self.database.convention.default_data 59 | 60 | for k, v in self.standard_dimensions.items(): 61 | i = 0 if k not in variances else 1 62 | if any(["S" in dims for dims in v]): 63 | var = self.create_string_array(k, v[i]) 64 | else: 65 | var = self.create_variable(k, v[i]) 66 | if k + ":Type" in default_values: var.Type = default_values[k + ":Type"] 67 | if k + ":Units" in default_values: var.Units = default_values[k + ":Units"] 68 | if k in default_values and default_values[k] != 0: 69 | var.set_values(default_values[k]) 70 | return -------------------------------------------------------------------------------- /src/sofa/roomtypes/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | """Classes for accessing RoomType-specific data. 22 | """ 23 | __all__ = ["implemented", "FreeField", "Reverberant", "Shoebox"] 24 | 25 | from .freefield import FreeField 26 | from .reverberant import Reverberant 27 | from .shoebox import Shoebox 28 | 29 | 30 | def implemented(): 31 | """Returns 32 | ------- 33 | list 34 | Names of implemented SOFA room types 35 | """ 36 | return list(List.keys()) 37 | 38 | 39 | List = { 40 | "free field": FreeField, 41 | "reverberant": Reverberant, 42 | "shoebox": Shoebox 43 | } 44 | 45 | 46 | def get(database): 47 | if database.RoomType in List.keys(): return List[database.dataset.RoomType](database) 48 | print("Unknown RoomType", database.DataType, ", returning free field instead") 49 | return List["free field"](database) 50 | -------------------------------------------------------------------------------- /src/sofa/roomtypes/base.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from .. import access 22 | 23 | class _Base(access.ProxyObject): 24 | def __init__(self, database): 25 | super().__init__(database, "Room") 26 | 27 | @property 28 | def Type(self): 29 | """SOFA data type""" 30 | return self.database.Metadata.get_attribute("RoomType") 31 | @Type.setter 32 | def Type(self, value): self.database.Metadata.set_attribute("RoomType", value) 33 | 34 | def optional_variance_names(self): 35 | """Returns a list of standardized data elements that may vary between measurements""" 36 | vardims = [] 37 | for k, v in self.standard_dimensions: 38 | if any(["I" in dims for dims in v]) and any(["M" in dims for dims in v]): vardims.append(k) 39 | return vardims 40 | 41 | def initialize(self, variances=[], string_length=None): 42 | """Create the necessary variables and attributes 43 | 44 | Parameters 45 | ---------- 46 | measurement_count : int 47 | Number of measurements 48 | sample_count : int 49 | Number of samples per measurement 50 | variances : list 51 | Names of the variables that vary along dimension M 52 | string_length : int, optional 53 | Size of the longest data string 54 | """ 55 | if string_length is not None: self.database.Dimensions.create_dimension("S", string_length) 56 | 57 | default_values = self.database._convention.default_data 58 | 59 | for k, v in self.standard_dimensions: 60 | i = 0 if k not in variances else 1 61 | if any(["S" in dims for dims in v]): 62 | var = self.create_string_array(k, v[i]) 63 | else: 64 | var = self.create_variable(k, v[i]) 65 | if k + ":Type" in default_values: var.Type = default_values[k + ":Type"] 66 | if k + ":Units" in default_values: var.Units = default_values[k + ":Units"] 67 | if k in default_values and default_values[k] != 0: 68 | var.set_values(default_values[k]) 69 | return -------------------------------------------------------------------------------- /src/sofa/roomtypes/freefield.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | """Classes for accessing RoomType-specific data. 22 | """ 23 | 24 | from .base import _Base 25 | 26 | class FreeField(_Base): 27 | pass -------------------------------------------------------------------------------- /src/sofa/roomtypes/reverberant.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | """Classes for accessing RoomType-specific data. 22 | """ 23 | 24 | from .base import _Base 25 | 26 | class Reverberant(_Base): 27 | def initialize(self, variances=[], string_length=None): 28 | super().initialize(variances, string_length) 29 | if "Description" in variances: self.create_string_array("Description", "M") 30 | else: self.create_attribute("Description") -------------------------------------------------------------------------------- /src/sofa/roomtypes/shoebox.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | """Classes for accessing RoomType-specific data. 22 | """ 23 | 24 | from .base import _Base 25 | from .. import spatial 26 | 27 | class Shoebox(_Base): 28 | 29 | """Shoebox room type 30 | 31 | CornerA : `sofa.spatial.Coordinates` 32 | First corner of room cuboid, dimensions ('I', 'C') or ('M', 'C') 33 | CornerB : `sofa.spatial.Coordinates` 34 | Opposite corner of room cuboid, dimensions ('I', 'C') or ('M', 'C') 35 | """ 36 | def __init__(self, database): 37 | super().__init__(database) 38 | self.standard_dimensions["CornerA"] = [("I", "C"), ("M", "C")] 39 | self.standard_dimensions["CornerB"] = [("I", "C"), ("M", "C")] 40 | 41 | @property 42 | def CornerA(self): 43 | """First corner of room cuboid""" 44 | return spatial.Coordinates(self, "CornerA") 45 | 46 | @property 47 | def CornerB(self): 48 | """Opposite corner of room cuboid""" 49 | return spatial.Coordinates(self, "CornerB") 50 | 51 | def initialize(self, variances=[], string_length=None): 52 | """Create the necessary variables and attributes 53 | 54 | Parameters 55 | ---------- 56 | measurement_count : int 57 | Number of measurements 58 | sample_count : int 59 | Number of samples per measurement 60 | variances : list 61 | Names of the variables that vary along dimension M 62 | string_length : int, optional 63 | Size of the longest data string 64 | """ 65 | self.CornerA.initialize("CornerA" in variances) 66 | self.CornerB.initialize("CornerB" in variances) 67 | -------------------------------------------------------------------------------- /src/sofa/spatial/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | """ 22 | """ 23 | __all__=["sph2cart", "cart2sph", "Units", "System", "Coordinates", "SpatialObject"] 24 | 25 | from .coordinates import * 26 | from .spatialobject import * 27 | -------------------------------------------------------------------------------- /src/sofa/spatial/coordinates.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from .. import access 22 | import numpy as np 23 | 24 | # for coordinate transformations 25 | from scipy.spatial.transform import Rotation ## requires scipy 1.2.0 26 | 27 | 28 | def sph2cart(alpha, beta, r): 29 | r"""Spherical to cartesian coordinate transform. 30 | 31 | .. math:: 32 | x = r \cos \alpha \sin \beta \\ 33 | y = r \sin \alpha \sin \beta \\ 34 | z = r \cos \beta 35 | 36 | with :math:`\alpha \in [0, 2\pi), \beta \in [-\frac{\pi}{2}, \frac{\pi}{2}], r \geq 0` 37 | 38 | Parameters 39 | ---------- 40 | alpha : float or array_like 41 | Azimuth angle in radians 42 | beta : float or array_like 43 | Elevation angle in radians (with 0 denoting azimuthal plane) 44 | r : float or array_like 45 | Radius 46 | 47 | Returns 48 | ------- 49 | x : float or `numpy.ndarray` 50 | x-component of Cartesian coordinates 51 | y : float or `numpy.ndarray` 52 | y-component of Cartesian coordinates 53 | z : float or `numpy.ndarray` 54 | z-component of Cartesian coordinates 55 | """ 56 | x = r * np.cos(alpha) * np.cos(beta) 57 | y = r * np.sin(alpha) * np.cos(beta) 58 | z = r * np.sin(beta) 59 | return x, y, z 60 | 61 | 62 | def cart2sph(x, y, z): 63 | r"""Cartesian to spherical coordinate transform. 64 | 65 | .. math:: 66 | \alpha = \arctan \left( \frac{y}{x} \right) \\ 67 | \beta = \arccos \left( \frac{z}{r} \right) \\ 68 | r = \sqrt{x^2 + y^2 + z^2} 69 | 70 | with :math:`\alpha \in [-pi, pi], \beta \in [-\frac{\pi}{2}, \frac{\pi}{2}], r \geq 0` 71 | 72 | Parameters 73 | ---------- 74 | x : float or array_like 75 | x-component of Cartesian coordinates 76 | y : float or array_like 77 | y-component of Cartesian coordinates 78 | z : float or array_like 79 | z-component of Cartesian coordinates 80 | 81 | Returns 82 | ------- 83 | alpha : float or `numpy.ndarray` 84 | Azimuth angle in radians 85 | beta : float or `numpy.ndarray` 86 | Elevation angle in radians (with 0 denoting azimuthal plane) 87 | r : float or `numpy.ndarray` 88 | Radius 89 | """ 90 | r = np.sqrt(x ** 2 + y ** 2 + z ** 2) 91 | alpha = np.arctan2(y, x) 92 | beta = np.arcsin(z / np.where(r != 0, r, 1)) 93 | return alpha, beta, r 94 | 95 | 96 | def transform(u, rot, x0, invert, is_position): 97 | if not invert and is_position: u = u - x0 98 | t = rot.apply(u, inverse=not invert) 99 | if invert and is_position: t = t + x0 100 | return t 101 | 102 | 103 | def _rotation_from_view_up(view, up): 104 | if len(view) != len(up): 105 | vlen = len(view) 106 | ulen = len(up) 107 | view = np.repeat(view, ulen, axis=0) 108 | up = np.repeat(up, vlen, axis=0) 109 | y_axis = np.cross(up, view) 110 | return Rotation.from_dcm(np.moveaxis(np.asarray([view, y_axis, up]), 0, -1)) 111 | 112 | 113 | def _get_object_transform(ref_object): 114 | order = ("M", "C") 115 | if ref_object is None: 116 | # global coordinate system 117 | position = np.asarray([[0, 0, 0]]) 118 | view = np.asarray([[1, 0, 0]]) 119 | up = np.asarray([[0, 0, 1]]) 120 | else: 121 | if ref_object.name == "Receiver" or ref_object.name == "Emitter": 122 | ldim = ref_object.Position.get_local_dimension() 123 | if ldim not in order: 124 | order = (ldim,) + order 125 | position, view, up = ref_object.get_pose(dim_order=order, system=System.Cartesian) 126 | 127 | if np.size(position.shape) < 3: 128 | def apply_transform(values, is_position, invert=False): 129 | rotation = _rotation_from_view_up(view, up) 130 | return transform(values, rotation, position, invert, is_position) 131 | else: 132 | def apply_transform(values, is_position, invert=False): 133 | rotations = [_rotation_from_view_up(v, u) for v, u in zip(view, up)] 134 | return np.asarray([transform(values, rot, pos, invert, is_position) for rot, pos in zip(rotations, position)]) 135 | return apply_transform, order 136 | 137 | 138 | class Units: 139 | @staticmethod 140 | def first_unit(unit_string): 141 | return unit_string.split((" "))[0].split((","))[0] 142 | 143 | @staticmethod 144 | def last_unit(unit_string): 145 | return unit_string.split((" "))[-1].split((","))[-1] 146 | 147 | Metre = "metre" # note: standard assumes british spelling 148 | Meter = "meter" 149 | 150 | @staticmethod 151 | def is_Metre(value): 152 | return Units.last_unit(value).lower() in Units._MeterAliases 153 | 154 | @staticmethod 155 | def is_Meter(value): 156 | return Units.last_unit(value).lower() in Units._MeterAliases 157 | 158 | _MeterAliases = {"meter", "meters", "metre", "metres", "m"} 159 | 160 | Degree = "degree" 161 | 162 | @staticmethod 163 | def is_Degree(value): 164 | return Units.first_unit(value).lower() in Units._DegreeAliases 165 | 166 | _DegreeAliases = {"degree", "degrees", "°", "deg"} 167 | 168 | Radians = "radians" # not intended for standard, but necessary for calculations 169 | 170 | @staticmethod 171 | def is_Radians(value): 172 | return Units.first_unit(value).lower() in Units._RadiansAliases 173 | 174 | _RadiansAliases = {"radians", "rad"} 175 | 176 | @staticmethod 177 | def convert_angle_units(coords, dimensions, old_units, new_units): 178 | """ 179 | Parameters 180 | ---------- 181 | coords : array_like 182 | Array of spherical coordinate values 183 | dimensions : tuple of str 184 | Names of the array dimensions in order, must contain "C" 185 | old_units : str 186 | Units of the angle values in the array 187 | new_units : str 188 | Target angle units 189 | 190 | Returns 191 | ------- 192 | new_coords : np.ndarray 193 | Array of converted spherical coordinate values in identical dimension order 194 | """ 195 | if dimensions is None: raise Exception("missing dimension order for unit conversion") 196 | if new_units is None: return coords 197 | if old_units is None: raise Exception("missing original unit for unit conversion") 198 | 199 | new_units = new_units.split((" "))[0].split((","))[0] 200 | 201 | conversion = np.ones_like(coords) 202 | indices = access.get_slice_tuple(dimensions, {"C": slice(2)}) 203 | 204 | if (Units.is_Degree(old_units) and Units.is_Degree(new_units)) or \ 205 | (Units.is_Radians(old_units) and Units.is_Radians(new_units)): 206 | return coords 207 | elif Units.is_Degree(old_units) and Units.is_Radians(new_units): 208 | conversion[indices] = np.pi / 180 209 | elif Units.is_Radians(old_units) and Units.is_Degree(new_units): 210 | conversion[indices] = 180 / np.pi 211 | else: 212 | raise Exception("invalid angle unit in conversion from {0} to {1}".format(old_units, new_units)) 213 | 214 | return np.multiply(coords, conversion) 215 | 216 | 217 | class System: 218 | """Enum of valid coordinate systems""" 219 | Cartesian = "cartesian" 220 | Spherical = "spherical" 221 | 222 | @staticmethod 223 | def convert(coords, dimensions, old_system, new_system, old_angle_unit=None, 224 | new_angle_unit=None): # need to take care of degree/radians unit mess. 225 | """ 226 | Parameters 227 | ---------- 228 | coords : array_like 229 | Array of coordinate values 230 | dimensions : tuple of str 231 | Names of the array dimensions in order, must contain "C" 232 | old_system : str 233 | Coordinate system of the values in the array 234 | new_system : str 235 | Target coordinate system 236 | old_angle_unit : str, optional 237 | Unit of the angular spherical coordinates 238 | new_angle_unit : str, optional 239 | Target unit of the angular spherical coordinates 240 | 241 | Returns 242 | ------- 243 | new_coords : np.ndarray 244 | Array of converted coordinate values in identical dimension order 245 | """ 246 | if dimensions is None: raise Exception("missing dimension order for coordinate conversion") 247 | if new_system is None or old_system == new_system: 248 | if old_system != System.Spherical: return coords 249 | return Units.convert_angle_units(coords, dimensions, old_angle_unit, new_angle_unit) 250 | old = None 251 | conversion = None 252 | if old_system == System.Cartesian and new_system == System.Spherical: 253 | conversion = cart2sph 254 | old = coords 255 | elif old_system == System.Spherical and new_system == System.Cartesian: 256 | conversion = sph2cart 257 | old = Units.convert_angle_units(coords, dimensions, old_angle_unit, Units.Radians) 258 | else: 259 | raise Exception("unknown coordinate conversion from {0} to {1}".format(old_system, new_system)) 260 | c_axis = dimensions.index("C") 261 | # a, b, c = np.split(old, 3, c_axis) 262 | a = old[access.get_slice_tuple(dimensions, {"C": 0})] 263 | b = old[access.get_slice_tuple(dimensions, {"C": 1})] 264 | c = old[access.get_slice_tuple(dimensions, {"C": 2})] 265 | new = np.moveaxis(np.asarray(conversion(a, b, c)), 0, c_axis) 266 | 267 | if new_system != System.Spherical: return new 268 | return Units.convert_angle_units(new, dimensions, Units.Radians, new_angle_unit) 269 | 270 | 271 | class Coordinates(access.Variable): 272 | """Specialized :class:`sofa.access.Variable` for spatial coordinates""" 273 | 274 | default_values = { 275 | "Position": (np.asarray([0, 0, 0]), System.Cartesian), 276 | "View": (np.asarray([1, 0, 0]), System.Cartesian), 277 | "Up": (np.asarray([0, 0, 1]), System.Cartesian), 278 | } 279 | 280 | def __init__(self, obj, descriptor): 281 | access.Variable.__init__(self, obj.database, obj.name + descriptor) 282 | self._obj_name = obj.name 283 | self._descriptor = descriptor 284 | if descriptor == "Up": self._unit_proxy = obj.View 285 | if descriptor == "CornerB": self._unit_proxy = obj.CornerA 286 | 287 | ldim = self.get_local_dimension() 288 | if ldim is None: 289 | self.standard_dimensions = [("I", "C"), ("M", "C")] 290 | else: 291 | self.standard_dimensions = [(ldim, "C", "I"), (ldim, "C", "M")] 292 | 293 | def initialize(self, varies, defaults=None): 294 | super().initialize(self.standard_dimensions[0 if not varies else 1]) 295 | if defaults is not None: 296 | values, system = defaults 297 | elif self._descriptor in self.default_values: 298 | values, system = self.default_values[self._descriptor] 299 | else: 300 | values, system = self.default_values["Position"] 301 | 302 | self.set_system(system) 303 | self.set_values(values, dim_order=("C",), 304 | repeat_dim=tuple([d for d in self.standard_dimensions[-1] if d != "C"])) 305 | 306 | @property 307 | def Type(self): 308 | """Coordinate system of the values""" 309 | if not self.exists(): 310 | raise Exception("failed to get Type of {0}, variable not initialized".format(self.name)) 311 | if self._unit_proxy == None: return self._Matrix.Type 312 | return self._unit_proxy._Matrix.Type 313 | 314 | @Type.setter 315 | def Type(self, value): 316 | if not self.exists(): 317 | raise Exception("failed to set Type of {0}, variable not initialized".format(self.name)) 318 | if type(value) == str: 319 | self._Matrix.Type = value 320 | return 321 | self._Matrix.Type = value.value 322 | 323 | def get_global_reference_object(self): 324 | if self._obj_name == "Receiver": return self.database.Listener 325 | if self._obj_name == "Emitter": return self.database.Source 326 | return None 327 | 328 | def get_local_dimension(self): 329 | if self._obj_name == "Receiver": return "R" 330 | if self._obj_name == "Emitter": return "E" 331 | return None 332 | 333 | def get_values(self, indices=None, dim_order=None, system=None, angle_unit=None): 334 | """Gets the coordinates in their original reference system 335 | 336 | Parameters 337 | ---------- 338 | indices : dict(key:str, value:int or slice), optional 339 | Key: dimension name, value: indices to be returned, complete axis assumed if not provided 340 | dim_order : tuple of str, optional 341 | Desired order of dimensions in the output array 342 | system : str, optional 343 | Target coordinate system 344 | angle_unit : str, optional 345 | Unit for spherical angles in the output array 346 | 347 | Returns 348 | ------- 349 | values : np.ndarray 350 | Coordinates in the original reference system 351 | """ 352 | values = access.Variable.get_values(self, indices, dim_order) 353 | if system is None or system == self.Type: 354 | if self.Type != System.Spherical or angle_unit == None: return values 355 | system = self.Type 356 | old_angle_unit = self.Units.split(",")[0] 357 | if angle_unit is None: angle_unit = Units.Degree 358 | if dim_order is None: dim_order = access.get_default_dimension_order(self.dimensions(), indices) 359 | return System.convert(values, dim_order, 360 | self.Type, system, 361 | old_angle_unit, angle_unit) 362 | 363 | def get_global_values(self, indices=None, dim_order=None, system=None, angle_unit=None): 364 | """Transform local coordinates (such as Receiver or Emitter) into the global reference system 365 | 366 | Parameters 367 | ---------- 368 | indices : dict(key:str, value:int or slice), optional 369 | Key: dimension name, value: indices to be returned, complete axis assumed if not provided 370 | dim_order : tuple of str, optional 371 | Desired order of dimensions in the output array 372 | system : str, optional 373 | Target coordinate system 374 | angle_unit : str, optional 375 | Unit for spherical angles in the output array 376 | 377 | Returns 378 | ------- 379 | global_values : np.ndarray 380 | Transformed coordinates in global reference system 381 | """ 382 | return self.get_relative_values(None, indices, dim_order, system, angle_unit) 383 | 384 | def get_relative_values(self, ref_object, indices=None, dim_order=None, system=None, angle_unit=None): 385 | """Transform coordinates (such as Receiver or Emitter) into the reference system of a given :class:`sofa.spatial.SpatialObject`, aligning the x-axis with View and the z-axis with Up 386 | 387 | Parameters 388 | ---------- 389 | ref_object : :class:`sofa.spatial.Object` 390 | Spatial object providing the reference system, None for 391 | indices : dict(key:str, value:int or slice), optional 392 | Key: dimension name, value: indices to be returned, complete axis assumed if not provided 393 | dim_order : tuple of str, optional 394 | Desired order of dimensions in the output array 395 | system : str, optional 396 | Target coordinate system 397 | angle_unit : str, optional 398 | Unit for spherical angles in the output array 399 | 400 | Returns 401 | ------- 402 | relative_values : np.ndarray 403 | Transformed coordinates in original or provided reference system 404 | """ 405 | if system is None: system = self.Type 406 | ldim = self.get_local_dimension() 407 | 408 | # get transforms 409 | anchor_transform, at_order = _get_object_transform(self.get_global_reference_object()) 410 | ref_transform, rt_order = _get_object_transform(ref_object) 411 | is_position = self._descriptor not in ["View", "Up"] 412 | 413 | # transform values 414 | if ldim is None: 415 | original_values = self.get_values(dim_order=at_order, system=System.Cartesian) 416 | transformed_values = ref_transform(anchor_transform(original_values, is_position, invert=True), is_position) 417 | order = rt_order 418 | else: 419 | original_values_stack = self.get_values(dim_order=(ldim,) + at_order, system=System.Cartesian) 420 | transformed_values = np.asarray( 421 | [ref_transform(anchor_transform(original_values, is_position, invert=True), is_position) for original_values in 422 | original_values_stack]) 423 | order = (ldim,) + rt_order 424 | 425 | # return in proper system, units and order 426 | if system == System.Spherical and angle_unit is None: 427 | angle_unit = self.Units.split(",")[0] if self.Type == System.Spherical else Units.Degree 428 | 429 | default_dimensions = self.dimensions() 430 | if len(rt_order) > 2: default_dimensions = (rt_order[0],) + default_dimensions 431 | 432 | if dim_order is None: dim_order = access.get_default_dimension_order(default_dimensions, indices) 433 | 434 | if indices is None or "C" not in indices: 435 | return System.convert(access.get_values_from_array(transformed_values, order, 436 | indices=indices, dim_order=dim_order), 437 | dim_order, 438 | System.Cartesian, system, 439 | new_angle_unit=angle_unit) 440 | else: # only apply "C" index after coordinate system conversion! 441 | return System.convert(access.get_values_from_array(transformed_values, order, 442 | indices={i: indices[i] for i in indices if 443 | i != "C"}, 444 | dim_order=("C",) + dim_order), 445 | ("C",) + dim_order, 446 | System.Cartesian, system, 447 | new_angle_unit=angle_unit)[indices["C"]] 448 | 449 | def set_system(self, ctype=None, cunits=None): 450 | """Set the coordinate Type and Units""" 451 | if ctype is None: ctype = System.Cartesian 452 | if type(ctype) != str: ctype = ctype.value 453 | self.Type = str(ctype) 454 | if cunits is None: 455 | if ctype == System.Cartesian: 456 | cunits = Units.Metre 457 | else: 458 | cunits = Units.Degree 459 | self.Units = str(cunits) 460 | 461 | def set_values(self, values, indices=None, dim_order=None, repeat_dim=None, system=None, angle_unit=None): 462 | """Sets the coordinate values after converting them to the system and units given by the dataset variable 463 | 464 | Parameters 465 | ---------- 466 | values : np.ndarray 467 | New values for the array range 468 | indices : dict(key:str, value:int or slice), optional 469 | Key: dimension name, value: indices to be set, complete axis assumed if not provided 470 | dim_order : tuple of str, optional 471 | Dimension names in provided order, regular order assumed 472 | repeat_dim : tuple of str, optional 473 | Tuple of dimension names along which to repeat the values 474 | system : str, optional 475 | Coordinate system of the provided values 476 | angle_unit : str, optional 477 | Angle units of the provided values 478 | """ 479 | if not self.exists(): 480 | raise Exception("failed to set values of {0}, variable not initialized".format(self.name)) 481 | 482 | if system is None: system = self.Type 483 | if angle_unit is None: angle_unit = self.Units 484 | 485 | if indices is not None and "C" in indices: 486 | iwoc = {i: indices[i] for i in indices if i != "C"} 487 | new_values, sls = self._reorder_values_for_set(values, 488 | indices=iwoc, 489 | dim_order=dim_order, 490 | repeat_dim=repeat_dim) 491 | new_order = access.get_default_dimension_order(self.dimensions(), iwoc) 492 | sls[new_order.index("C")] = indices["C"] 493 | self._Matrix[sls] = System.convert(new_values, 494 | new_order, 495 | system, self.Type, 496 | angle_unit, self.Units 497 | )[access.get_slice_tuple(new_order, {"C": indices["C"]})] 498 | else: 499 | new_values, sls = self._reorder_values_for_set(values, indices, dim_order, repeat_dim) 500 | new_order = access.get_default_dimension_order(self.dimensions(), indices) 501 | self._Matrix[sls] = System.convert(new_values, new_order, system, self.Type, angle_unit, self.Units) 502 | -------------------------------------------------------------------------------- /src/sofa/spatial/spatialobject.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 Jannika Lossner 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | from .. import access 22 | from .coordinates import * 23 | import numpy as np 24 | 25 | 26 | class SpatialObject(access.ProxyObject): 27 | """Spatial object such as Listener, Receiver, Source, Emitter""" 28 | 29 | def __init__(self, database, name): 30 | super().__init__(database, name) 31 | return 32 | 33 | @property 34 | def Position(self): 35 | """Position of the spatial object relative to its reference system""" 36 | return Coordinates(self, "Position") 37 | 38 | @property 39 | def View(self): 40 | """View (x-axis) of the spatial object relative to its reference system""" 41 | return Coordinates(self, "View") 42 | 43 | @property 44 | def Up(self): 45 | """Up (z-axis) of the spatial object relative to its reference system""" 46 | return Coordinates(self, "Up") 47 | 48 | @property 49 | def Type(self): 50 | """Coordinate syste, of the values""" 51 | if not self.exists(): 52 | raise Exception("failed to get Type of {0}, variable not initialized".format(self.name)) 53 | if self._unit_proxy is None: return self._Matrix.Type 54 | return self._unit_proxy.Type 55 | @Type.setter 56 | def Type(self, value): 57 | if not self.exists(): 58 | raise Exception("failed to set Type of {0}, variable not initialized".format(self.name)) 59 | self._Matrix.Type = value 60 | 61 | def initialize(self, fixed=[], variances=[], count=None): 62 | """Create the necessary variables and attributes 63 | 64 | Parameters 65 | ---------- 66 | fixed : list(str), optional 67 | List of spatial coordinates that are fixed for all measurements ["Position", "View", "Up"] 68 | variances : list(str), optional 69 | List of spatial coordinates that vary between measurements ["Position", "View", "Up"], 70 | overrides mentions in fixed 71 | count : int, optional 72 | Number of objects (such as Emitters or Receivers), ignored for Listener and Source 73 | """ 74 | mentioned = fixed + variances 75 | if "Position" not in mentioned: 76 | if not self.Position.exists(): raise ValueError("{0}.initialize: Missing 'Position' in fixed or variances argument".format(self.name)) 77 | if "Up" in mentioned and "View" not in mentioned: 78 | if not self.View.exists(): raise ValueError("{0}.initialize: Missing 'View' in fixed or variances argument".format(self.name)) 79 | 80 | ldim = self.Position.get_local_dimension() 81 | if ldim is None: 82 | if count is None: count = 1 83 | else: 84 | if count is None and ldim in self.database.Dimensions.list_dimensions(): 85 | count = self.database.Dimensions.get_dimension(ldim) 86 | if count is None and "count" in self.database.convention.default_objects[self.name].keys(): 87 | count = self.database.convention.default_objects[self.name]["count"] 88 | if count is None: raise Exception(self.name, "{0} count missing!".format(self.name)) 89 | self.database.convention.validate_spatial_object_settings(self.name, fixed, variances, count) 90 | if ldim is not None and ldim not in self.database.Dimensions.list_dimensions(): 91 | self.database.Dimensions.create_dimension(ldim, count) 92 | 93 | self.create_attribute("Description") 94 | self.initialize_coordinates(fixed, variances) 95 | self.database.convention.set_default_spatial_values(self) 96 | 97 | def initialize_coordinates(self, fixed=[], variances=[]): 98 | """ 99 | Parameters 100 | ---------- 101 | fixed : list(str), optional 102 | List of spatial coordinates that are fixed for all measurements ["Position", "View", "Up"] 103 | variances : list(str), optional 104 | List of spatial coordinates that vary between measurements ["Position", "View", "Up"], 105 | overrides mentions in fixed 106 | """ 107 | mentioned = fixed + variances 108 | for c in mentioned: 109 | var = self.__getattribute__(c) 110 | if not var.exists(): var.initialize(c in variances) 111 | 112 | def get_pose(self, indices=None, dim_order=None, system=None, angle_unit=None): 113 | """ Gets the spatial object coordinates or their defaults if they have not been defined. Relative spatial objects return their global pose, or their reference object's pose values if theirs are undefined. 114 | 115 | Parameters 116 | ---------- 117 | indices : dict(key:str, value:int or slice), optional 118 | Key: dimension name, value: indices to be returned, complete axis assumed if not provided 119 | dim_order : tuple of str, optional 120 | Desired order of dimensions in the output arrays 121 | system : str, optional 122 | Target coordinate system 123 | angle_unit : str, optional 124 | Unit for spherical angles in the output arrays 125 | 126 | Returns 127 | ------- 128 | position, view, up : np.ndarray, np.ndarray, np.ndarray 129 | Spatial object reference system 130 | 131 | """ 132 | 133 | if angle_unit is None: angle_unit = "rad" 134 | anchor = self.Position.get_global_reference_object() 135 | if anchor is None: # this is an object in the global coordinate system 136 | default_order = ("I", "C") 137 | position = np.asarray([[0, 0, 0]]) 138 | view = np.asarray([[1, 0, 0]]) 139 | up = np.asarray([[0, 0, 1]]) 140 | 141 | else: # this is an object defined relative to another 142 | ldim = self.Position.get_local_dimension() 143 | lcount = self.database.Dimensions.get_dimension(ldim) 144 | 145 | anchor_order = ("I", "C") 146 | default_order = (ldim,) + anchor_order 147 | if dim_order is None: dim_order = access.get_default_dimension_order(self.Position.dimensions(), indices) 148 | 149 | anchor_position, anchor_view, anchor_up = anchor.get_pose(indices=indices, dim_order=anchor_order, 150 | system=System.Cartesian) 151 | position = np.repeat(np.expand_dims(anchor_position, 0), lcount, axis=0) 152 | view = np.repeat(np.expand_dims(anchor_view, 0), lcount, axis=0) 153 | up = np.repeat(np.expand_dims(anchor_up, 0), lcount, axis=0) 154 | 155 | # get existing values or use defaults 156 | if self.Position.exists(): 157 | position = self.Position.get_global_values(indices, dim_order, system, angle_unit) 158 | else: 159 | position = access.get_values_from_array( 160 | System.convert(position, default_order, System.Cartesian, system, angle_unit, 161 | angle_unit), 162 | default_order, dim_order=dim_order) 163 | 164 | if self.View.exists(): 165 | view = self.View.get_global_values(indices, dim_order, system, angle_unit) 166 | else: 167 | view = access.get_values_from_array( 168 | System.convert(view, default_order, System.Cartesian, system, angle_unit, 169 | angle_unit), 170 | default_order, dim_order=dim_order) 171 | 172 | if self.Up.exists(): 173 | up = self.Up.get_global_values(indices, dim_order, system, angle_unit) 174 | else: 175 | up = access.get_values_from_array( 176 | System.convert(up, default_order, System.Cartesian, system, angle_unit, 177 | angle_unit), 178 | default_order, dim_order=dim_order) 179 | 180 | return position, view, up 181 | --------------------------------------------------------------------------------