├── .dvc ├── .gitignore └── config ├── .dvcignore ├── .git_archival.txt ├── .gitattributes ├── .github ├── CONTRIBUTING.md ├── dependabot.yml ├── matchers │ └── pylint.json └── workflows │ ├── cd.yml │ └── ci.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── LICENSE ├── README.rst ├── db_dvc.dvc ├── docs ├── Makefile ├── _static │ └── .gitkeep ├── _templates │ ├── custom-class-template.rst │ └── custom-module-template.rst ├── api.rst ├── conf.py ├── hv_controller_api.md ├── includeme.rst ├── index.rst ├── make.bat └── tx_device_api.md ├── notebooks ├── LIFUTestWidget.py ├── OpenLIFU_2x_1.json ├── README.md ├── pinmap.json ├── run_self_test.py ├── stress_test.py ├── test_console.py ├── test_console2.py ├── test_console_dfu.py ├── test_first.py ├── test_multiple_modules.py ├── test_nifti.py ├── test_nucleo.py ├── test_pwr.py ├── test_registers.py ├── test_reset.py ├── test_solution.py ├── test_solution_analysis.py ├── test_temp.py ├── test_thermal_stress.py ├── test_ti_cfg.py ├── test_toggle_12v.py ├── test_transmitter.py ├── test_transmitter2.py ├── test_tx_dfu.py ├── test_tx_trigger.py ├── test_updated_if.py ├── test_updated_pwr.py ├── test_watertank.py └── ti_example.cfg ├── noxfile.py ├── pyproject.toml ├── scripts └── standardize_database.py ├── src └── openlifu │ ├── __init__.py │ ├── _version.pyi │ ├── bf │ ├── __init__.py │ ├── apod_methods │ │ ├── __init__.py │ │ ├── apodmethod.py │ │ ├── maxangle.py │ │ ├── piecewiselinear.py │ │ └── uniform.py │ ├── delay_methods │ │ ├── __init__.py │ │ ├── delaymethod.py │ │ └── direct.py │ ├── focal_patterns │ │ ├── __init__.py │ │ ├── focal_pattern.py │ │ ├── single.py │ │ └── wheel.py │ ├── pulse.py │ └── sequence.py │ ├── db │ ├── __init__.py │ ├── database.py │ ├── session.py │ ├── subject.py │ └── user.py │ ├── geo.py │ ├── io │ ├── LIFUConfig.py │ ├── LIFUHVController.py │ ├── LIFUInterface.py │ ├── LIFUSignal.py │ ├── LIFUTXDevice.py │ ├── LIFUUart.py │ └── __init__.py │ ├── nav │ ├── __init__.py │ ├── meshroom_pipelines │ │ ├── __init__.py │ │ ├── default_pipeline.mg │ │ ├── downsample_1x_pipeline.mg │ │ ├── downsample_4x_pipeline.mg │ │ ├── downsample_8x_pipeline.mg │ │ └── draft_pipeline.mg │ ├── modnet_checkpoints │ │ └── __init__.py │ └── photoscan.py │ ├── plan │ ├── __init__.py │ ├── param_constraint.py │ ├── protocol.py │ ├── run.py │ ├── solution.py │ ├── solution_analysis.py │ └── target_constraints.py │ ├── py.typed │ ├── seg │ ├── __init__.py │ ├── material.py │ ├── seg_method.py │ ├── seg_methods │ │ ├── __init__.py │ │ └── uniform.py │ └── skinseg.py │ ├── sim │ ├── __init__.py │ ├── kwave_if.py │ └── sim_setup.py │ ├── util │ ├── annotations.py │ ├── checkgpu.py │ ├── dict_conversion.py │ ├── json.py │ ├── strings.py │ └── units.py │ ├── virtual_fit.py │ └── xdc │ ├── __init__.py │ ├── element.py │ └── transducer.py └── tests ├── helpers.py ├── resources └── example_db │ ├── protocols │ ├── example_protocol │ │ └── example_protocol.json │ └── protocols.json │ ├── subjects │ ├── example_subject │ │ ├── example_subject.json │ │ ├── sessions │ │ │ ├── example_session │ │ │ │ ├── example_session.json │ │ │ │ ├── photoscans │ │ │ │ │ ├── example_photoscan │ │ │ │ │ │ ├── example_photoscan.json │ │ │ │ │ │ ├── example_photoscan.obj │ │ │ │ │ │ └── example_photoscan_texture.exr │ │ │ │ │ └── photoscans.json │ │ │ │ ├── runs │ │ │ │ │ ├── example_run │ │ │ │ │ │ ├── example_run.json │ │ │ │ │ │ ├── example_run_protocol_snapshot.json │ │ │ │ │ │ └── example_run_session_snapshot.json │ │ │ │ │ └── runs.json │ │ │ │ └── solutions │ │ │ │ │ ├── example_solution │ │ │ │ │ ├── example_solution.json │ │ │ │ │ ├── example_solution.nc │ │ │ │ │ └── example_solution_analysis.json │ │ │ │ │ └── solutions.json │ │ │ └── sessions.json │ │ └── volumes │ │ │ ├── example_volume │ │ │ ├── example_volume.json │ │ │ └── example_volume.nii │ │ │ └── volumes.json │ └── subjects.json │ ├── systems │ └── systems.json │ ├── transducers │ ├── example_transducer │ │ └── example_transducer.json │ └── transducers.json │ └── users │ ├── example_user │ └── example_user.json │ └── users.json ├── test_apod_methods.py ├── test_database.py ├── test_geo.py ├── test_io.py ├── test_material.py ├── test_offset_grid.py ├── test_package.py ├── test_param_constraints.py ├── test_photoscans.py ├── test_point.py ├── test_protocol.py ├── test_run.py ├── test_seg_method.py ├── test_sequence.py ├── test_sim.py ├── test_skinseg.py ├── test_solution.py ├── test_solution_analysis.py ├── test_sonication_control_mock.py ├── test_transducer.py ├── test_units.py ├── test_user.py └── test_virtual_fit.py /.dvc/.gitignore: -------------------------------------------------------------------------------- 1 | /config.local 2 | /tmp 3 | /cache 4 | -------------------------------------------------------------------------------- /.dvc/config: -------------------------------------------------------------------------------- 1 | [core] 2 | remote = shared_gdrive 3 | autostage = true 4 | ['remote "shared_gdrive"'] 5 | url = gdrive://1rGAzqdikeTUzUQADjdP7-fTPdfF15eaO 6 | gdrive_acknowledge_abuse = true 7 | gdrive_client_id = 78626142150-1ikhqfnmr5p1aa63ji5opa7gveeksn90.apps.googleusercontent.com 8 | -------------------------------------------------------------------------------- /.dvcignore: -------------------------------------------------------------------------------- 1 | # Add patterns of files dvc should ignore, which could improve 2 | # the performance. Learn more at 3 | # https://dvc.org/doc/user-guide/dvcignore 4 | -------------------------------------------------------------------------------- /.git_archival.txt: -------------------------------------------------------------------------------- 1 | node: c83a1337118e78051dc79b6c68eaef3d5f781ac9 2 | node-date: 2025-06-10T16:37:54-04:00 3 | describe-name: v0.6.0-17-gc83a1337 4 | ref-names: HEAD -> main 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | .git_archival.txt export-subst 2 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | See the [Scientific Python Developer Guide][spc-dev-intro] for a detailed 2 | description of best practices for developing scientific packages. 3 | 4 | [spc-dev-intro]: https://learn.scientific-python.org/development/ 5 | 6 | # Commit and pull request expectations 7 | 8 | - Every commit should reference the issue number with which it is associated. 9 | - Commits should be reasonably granular and semantically atomic. 10 | - Pull requests should not be squashed upon merging. 11 | 12 | # Quick development 13 | 14 | The fastest way to start with development is to use nox. If you don't have nox, 15 | you can use `pipx run nox` to run it without installing, or `pipx install nox`. 16 | If you don't have pipx (pip for applications), then you can install with 17 | `pip install pipx` (the only case were installing an application with regular 18 | pip is reasonable). If you use macOS, then pipx and nox are both in brew, use 19 | `brew install pipx nox`. 20 | 21 | To use, run `nox`. This will lint and test using every installed version of 22 | Python on your system, skipping ones that are not installed. You can also run 23 | specific jobs: 24 | 25 | ```console 26 | $ nox -s lint # Lint only 27 | $ nox -s tests # Python tests 28 | $ nox -s docs -- --serve # Build and serve the docs 29 | $ nox -s build # Make an SDist and wheel 30 | ``` 31 | 32 | Nox handles everything for you, including setting up an temporary virtual 33 | environment for each run. 34 | 35 | # Setting up a development environment manually 36 | 37 | You can set up a development environment by running: 38 | 39 | ```bash 40 | python3 -m venv .venv 41 | source ./.venv/bin/activate 42 | pip install -v -e '.[dev]' 43 | ``` 44 | 45 | If you have the 46 | [Python Launcher for Unix](https://github.com/brettcannon/python-launcher), you 47 | can instead do: 48 | 49 | ```bash 50 | py -m venv .venv 51 | py -m install -v -e '.[dev]' 52 | ``` 53 | 54 | # Post setup 55 | 56 | You should prepare pre-commit, which will help you by checking that commits pass 57 | required checks: 58 | 59 | ```bash 60 | pip install pre-commit # or brew install pre-commit on macOS 61 | pre-commit install # Will install a pre-commit hook into the git repo 62 | ``` 63 | 64 | You can also/alternatively run `pre-commit run` (changes only) or 65 | `pre-commit run --all-files` to check even without installing the hook. 66 | 67 | # Testing 68 | 69 | Use pytest to run the unit checks: 70 | 71 | ```bash 72 | pytest 73 | ``` 74 | 75 | # Coverage 76 | 77 | Use pytest-cov to generate coverage reports: 78 | 79 | ```bash 80 | pytest --cov=openlifu 81 | ``` 82 | 83 | # Building docs 84 | 85 | You can build the docs using: 86 | 87 | ```bash 88 | nox -s docs 89 | ``` 90 | 91 | You can see a preview with: 92 | 93 | ```bash 94 | nox -s docs -- --serve 95 | ``` 96 | 97 | # Pre-commit 98 | 99 | This project uses pre-commit for all style checking. While you can run it with 100 | nox, this is such an important tool that it deserves to be installed on its own. 101 | Install pre-commit and run: 102 | 103 | ```bash 104 | pre-commit run -a 105 | ``` 106 | 107 | to check all files. 108 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | groups: 9 | actions: 10 | patterns: 11 | - "*" 12 | 13 | # Maintain dependencies for Python 14 | - package-ecosystem: "pip" 15 | directory: "/" 16 | schedule: 17 | interval: "weekly" 18 | -------------------------------------------------------------------------------- /.github/matchers/pylint.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "severity": "warning", 5 | "pattern": [ 6 | { 7 | "regexp": "^([^:]+):(\\d+):(\\d+): ([A-DF-Z]\\d+): \\033\\[[\\d;]+m([^\\033]+).*$", 8 | "file": 1, 9 | "line": 2, 10 | "column": 3, 11 | "code": 4, 12 | "message": 5 13 | } 14 | ], 15 | "owner": "pylint-warning" 16 | }, 17 | { 18 | "severity": "error", 19 | "pattern": [ 20 | { 21 | "regexp": "^([^:]+):(\\d+):(\\d+): (E\\d+): \\033\\[[\\d;]+m([^\\033]+).*$", 22 | "file": 1, 23 | "line": 2, 24 | "column": 3, 25 | "code": 4, 26 | "message": 5 27 | } 28 | ], 29 | "owner": "pylint-error" 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CD 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | push: 7 | branches: 8 | - main 9 | release: 10 | types: 11 | - published 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | env: 18 | # Many color libraries just need this to be set to any value, but at least 19 | # one distinguishes color depth, where "3" -> "256-bit color". 20 | FORCE_COLOR: 3 21 | 22 | jobs: 23 | dist: 24 | name: Distribution build 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - uses: actions/checkout@v4 29 | with: 30 | fetch-depth: 0 31 | 32 | - uses: hynek/build-and-inspect-python-package@v2 33 | 34 | publish: 35 | needs: [dist] 36 | name: Publish to PyPI 37 | environment: pypi 38 | permissions: 39 | id-token: write 40 | runs-on: ubuntu-latest 41 | if: github.event_name == 'release' && github.event.action == 'published' 42 | 43 | steps: 44 | - uses: actions/download-artifact@v4 45 | with: 46 | name: Packages 47 | path: dist 48 | 49 | - uses: pypa/gh-action-pypi-publish@release/v1 50 | if: github.event_name == 'release' && github.event.action == 'published' 51 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | push: 7 | branches: 8 | - main 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | env: 15 | # Many color libraries just need this to be set to any value, but at least 16 | # one distinguishes color depth, where "3" -> "256-bit color". 17 | FORCE_COLOR: 3 18 | 19 | jobs: 20 | pre-commit: 21 | name: Format 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | - uses: actions/setup-python@v5 28 | with: 29 | python-version: "3.x" 30 | - uses: pre-commit/action@v3.0.1 31 | with: 32 | extra_args: --hook-stage manual --all-files 33 | - name: Run PyLint 34 | run: | 35 | echo "::add-matcher::$GITHUB_WORKSPACE/.github/matchers/pylint.json" 36 | pipx run nox -s pylint 37 | 38 | checks: 39 | name: Check Python ${{ matrix.python-version }} on ${{ matrix.runs-on }} 40 | runs-on: ${{ matrix.runs-on }} 41 | needs: [pre-commit] 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | python-version: ["3.9", "3.12"] 46 | runs-on: [ubuntu-latest, windows-latest, macos-latest] 47 | 48 | steps: 49 | - uses: actions/checkout@v4 50 | with: 51 | fetch-depth: 0 52 | 53 | - uses: actions/setup-python@v5 54 | with: 55 | python-version: ${{ matrix.python-version }} 56 | allow-prereleases: true 57 | 58 | - name: Install system dependencies 59 | if: runner.os == 'macOS' 60 | run: brew install hdf5 fftw zlib libomp 61 | 62 | - name: Install package 63 | run: python -m pip install .[test] 64 | 65 | - name: Test package 66 | run: >- 67 | python -m pytest -ra --cov --cov-report=xml --cov-report=term 68 | --durations=20 69 | 70 | - name: Upload coverage report 71 | uses: codecov/codecov-action@v5.4.3 72 | with: 73 | token: ${{ secrets.CODECOV_TOKEN }} 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | docs/_autosummary/ 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | *.ipynb 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | # For a library or package, you might want to ignore these files since the code is 89 | # intended to run in multiple environments; otherwise, check them in: 90 | # .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 100 | __pypackages__/ 101 | 102 | # Celery stuff 103 | celerybeat-schedule 104 | celerybeat.pid 105 | 106 | # SageMath parsed files 107 | *.sage.py 108 | 109 | # Environments 110 | .env 111 | .venv 112 | env/ 113 | venv/ 114 | ENV/ 115 | env.bak/ 116 | venv.bak/ 117 | 118 | # Spyder project settings 119 | .spyderproject 120 | .spyproject 121 | 122 | # Rope project settings 123 | .ropeproject 124 | 125 | # mkdocs documentation 126 | /site 127 | 128 | # mypy 129 | .mypy_cache/ 130 | .dmypy.json 131 | dmypy.json 132 | 133 | # Pyre type checker 134 | .pyre/ 135 | 136 | # pytype static type analyzer 137 | .pytype/ 138 | 139 | # Cython debug symbols 140 | cython_debug/ 141 | 142 | # setuptools_scm 143 | src/*/_version.py 144 | 145 | 146 | # ruff 147 | .ruff_cache/ 148 | 149 | # OS specific stuff 150 | .DS_Store 151 | .DS_Store? 152 | ._* 153 | .Spotlight-V100 154 | .Trashes 155 | ehthumbs.db 156 | Thumbs.db 157 | 158 | # Common editor files 159 | *~ 160 | *.swp 161 | .vscode 162 | /db_dvc 163 | 164 | # ONNX checkpoints 165 | *.onnx 166 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | autoupdate_commit_msg: "chore: update pre-commit hooks" 3 | autofix_commit_msg: "style: pre-commit fixes" 4 | 5 | repos: 6 | - repo: https://github.com/adamchainz/blacken-docs 7 | rev: "1.16.0" 8 | hooks: 9 | - id: blacken-docs 10 | additional_dependencies: [black==24.*] 11 | 12 | - repo: https://github.com/pre-commit/pre-commit-hooks 13 | rev: "v4.6.0" 14 | hooks: 15 | - id: check-added-large-files 16 | - id: check-case-conflict 17 | - id: check-merge-conflict 18 | - id: check-symlinks 19 | - id: check-yaml 20 | - id: debug-statements 21 | - id: end-of-file-fixer 22 | - id: mixed-line-ending 23 | - id: name-tests-test 24 | args: ["--pytest-test-first"] 25 | exclude: ^tests/helpers\.py$ 26 | - id: requirements-txt-fixer 27 | - id: trailing-whitespace 28 | 29 | - repo: https://github.com/pre-commit/pygrep-hooks 30 | rev: "v1.10.0" 31 | hooks: 32 | - id: rst-backticks 33 | - id: rst-directive-colons 34 | - id: rst-inline-touching-normal 35 | 36 | - repo: https://github.com/pre-commit/mirrors-prettier 37 | rev: "v3.1.0" 38 | hooks: 39 | - id: prettier 40 | types_or: [yaml, markdown, html, css, scss, javascript, json] 41 | args: [--prose-wrap=always] 42 | 43 | - repo: https://github.com/astral-sh/ruff-pre-commit 44 | rev: "v0.4.1" 45 | hooks: 46 | - id: ruff 47 | args: ["--fix", "--show-fixes"] 48 | # - id: ruff-format # Omitting ruff-format for now 49 | 50 | # Disabling static typechecking for now! 51 | # - repo: https://github.com/pre-commit/mirrors-mypy 52 | # rev: "v1.9.0" 53 | # hooks: 54 | # - id: mypy 55 | # files: src|tests 56 | # args: [] 57 | # additional_dependencies: 58 | # - pytest 59 | 60 | - repo: https://github.com/codespell-project/codespell 61 | rev: "v2.2.6" 62 | hooks: 63 | - id: codespell 64 | exclude: .*\.ipynb # exclude notebooks because images are sometimes captured in spell check 65 | 66 | - repo: https://github.com/shellcheck-py/shellcheck-py 67 | rev: "v0.10.0.1" 68 | hooks: 69 | - id: shellcheck 70 | 71 | - repo: local 72 | hooks: 73 | - id: disallow-caps 74 | name: Disallow improper capitalization 75 | language: pygrep 76 | entry: PyBind|Numpy|Cmake|CCache|Github|PyTest 77 | exclude: .pre-commit-config.yaml 78 | 79 | - repo: https://github.com/abravalheri/validate-pyproject 80 | rev: "v0.16" 81 | hooks: 82 | - id: validate-pyproject 83 | additional_dependencies: ["validate-pyproject-schema-store[all]"] 84 | 85 | - repo: https://github.com/python-jsonschema/check-jsonschema 86 | rev: "0.28.2" 87 | hooks: 88 | - id: check-dependabot 89 | - id: check-github-workflows 90 | - id: check-readthedocs 91 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | version: 2 5 | 6 | build: 7 | os: ubuntu-22.04 8 | tools: 9 | python: "3.9" 10 | sphinx: 11 | configuration: docs/conf.py 12 | 13 | python: 14 | install: 15 | - method: pip 16 | path: . 17 | extra_requirements: 18 | - docs 19 | -------------------------------------------------------------------------------- /db_dvc.dvc: -------------------------------------------------------------------------------- 1 | outs: 2 | - md5: 9fccb7e2954839d729ab3d0fa1abb580.dir 3 | nfiles: 797 4 | hash: md5 5 | path: db_dvc 6 | size: 2357262767 7 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/_static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenwaterHealth/OpenLIFU-python/c83a1337118e78051dc79b6c68eaef3d5f781ac9/docs/_static/.gitkeep -------------------------------------------------------------------------------- /docs/_templates/custom-class-template.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline}} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. autoclass:: {{ objname }} 6 | :members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/_templates/custom-module-template.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline}} 2 | 3 | .. automodule:: {{ fullname }} 4 | 5 | {% block attributes %} 6 | {% if attributes %} 7 | .. rubric:: Module Attributes 8 | 9 | .. autosummary:: 10 | :toctree: 11 | {% for item in attributes %} 12 | {{ item }} 13 | {%- endfor %} 14 | {% endif %} 15 | {% endblock %} 16 | 17 | {% block functions %} 18 | {% if functions %} 19 | .. rubric:: {{ _('Functions') }} 20 | 21 | .. autosummary:: 22 | :toctree: 23 | {% for item in functions %} 24 | {{ item }} 25 | {%- endfor %} 26 | {% endif %} 27 | {% endblock %} 28 | 29 | {% block classes %} 30 | {% if classes %} 31 | .. rubric:: {{ _('Classes') }} 32 | 33 | .. autosummary:: 34 | :toctree: 35 | :template: custom-class-template.rst 36 | {% for item in classes %} 37 | {{ item }} 38 | {%- endfor %} 39 | {% endif %} 40 | {% endblock %} 41 | 42 | {% block exceptions %} 43 | {% if exceptions %} 44 | .. rubric:: {{ _('Exceptions') }} 45 | 46 | .. autosummary:: 47 | :toctree: 48 | {% for item in exceptions %} 49 | {{ item }} 50 | {%- endfor %} 51 | {% endif %} 52 | {% endblock %} 53 | 54 | {% block modules %} 55 | {% if modules %} 56 | .. rubric:: Modules 57 | 58 | .. autosummary:: 59 | :toctree: 60 | :template: custom-module-template.rst 61 | :recursive: 62 | {% for item in modules %} 63 | {{ item }} 64 | {%- endfor %} 65 | {% endif %} 66 | {% endblock %} 67 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | The top-level module containing most of the code is ``openlifu`` 5 | 6 | .. autosummary:: 7 | :toctree: _autosummary 8 | :template: custom-module-template.rst 9 | :recursive: 10 | 11 | openlifu 12 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | from __future__ import annotations 9 | 10 | import importlib.metadata 11 | 12 | project = 'openlifu' 13 | copyright = '2023, Openwater' 14 | author = 'Openwater' 15 | version = release = importlib.metadata.version("openlifu") 16 | 17 | import sys 18 | from pathlib import Path 19 | 20 | sys.path.insert(0, Path('..').resolve()) 21 | 22 | # -- General configuration --------------------------------------------------- 23 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 24 | 25 | extensions = [ 26 | 'myst_parser', 27 | 'sphinx.ext.autodoc', 28 | 'sphinx.ext.autosummary', 29 | 'sphinx.ext.doctest', 30 | 'sphinx.ext.intersphinx', 31 | 'sphinx.ext.todo', 32 | 'sphinx.ext.coverage', 33 | 'sphinx.ext.mathjax', 34 | 'sphinx.ext.ifconfig', 35 | 'sphinx.ext.viewcode', 36 | 'sphinx.ext.githubpages', 37 | 'sphinx.ext.napoleon', 38 | ] 39 | 40 | source_suffix = [".rst", ".md"] 41 | templates_path = ['_templates'] 42 | exclude_patterns = [ 43 | "_build", 44 | "_templates", 45 | "**.ipynb_checkpoints", 46 | "Thumbs.db", 47 | ".DS_Store", 48 | ".env", 49 | ".venv", 50 | ] 51 | 52 | autosummary_generate = True # Turn on sphinx.ext.autosummary 53 | 54 | # Napoleon settings 55 | napoleon_google_docstring = True 56 | napoleon_numpy_docstring = True 57 | napoleon_include_init_with_doc = True 58 | napoleon_include_private_with_doc = True 59 | napoleon_include_special_with_doc = True 60 | napoleon_use_admonition_for_examples = False 61 | napoleon_use_admonition_for_notes = False 62 | napoleon_use_admonition_for_references = False 63 | napoleon_use_ivar = False 64 | napoleon_use_param = True 65 | napoleon_use_rtype = True 66 | 67 | # -- Options for HTML output ------------------------------------------------- 68 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 69 | 70 | html_theme = 'alabaster' 71 | html_static_path = ['_static'] 72 | 73 | myst_enable_extensions = [ 74 | "colon_fence", 75 | ] 76 | -------------------------------------------------------------------------------- /docs/hv_controller_api.md: -------------------------------------------------------------------------------- 1 | # HVController API Documentation 2 | 3 | The `HVController` class provides an interface to control and monitor a 4 | high-voltage console device over UART using the `LIFUUart` interface. 5 | 6 | --- 7 | 8 | ## Initialization 9 | 10 | ```python 11 | from openlifu.io.LIFUHVController import HVController 12 | from openlifu.io.LIFUUart import LIFUUart 13 | 14 | interface = LIFUInterface(TX_test_mode=False) 15 | tx_connected, hv_connected = interface.is_device_connected() 16 | if tx_connected and hv_connected: 17 | print("LIFU Device Fully connected.") 18 | else: 19 | print(f"LIFU Device NOT Fully Connected. TX: {tx_connected}, HV: {hv_connected}") 20 | 21 | if not hv_connected: 22 | print("HV Controller not connected.") 23 | sys.exit() 24 | ``` 25 | 26 | --- 27 | 28 | ## Methods 29 | 30 | ### Device Info & Connectivity 31 | 32 | | Method | Description | 33 | | ------------------- | ----------------------------------------------- | 34 | | `is_connected()` | Check if UART is connected | 35 | | `ping()` | Sends a ping to check device responsiveness | 36 | | `get_version()` | Returns firmware version as `vX.Y.Z` | 37 | | `get_hardware_id()` | Returns the 16-byte hardware ID as a hex string | 38 | | `echo(data: bytes)` | Echoes back sent data, useful for testing | 39 | | `soft_reset()` | Sends a soft reset to the device | 40 | | `enter_dfu()` | Put the device into DFU mode | 41 | 42 | --- 43 | 44 | ### Voltage Control 45 | 46 | | Method | Description | 47 | | -------------------------------- | -------------------------------------------- | 48 | | `set_voltage(voltage: float)` | Sets output voltage (5V–100V range) | 49 | | `get_voltage()` | Reads and returns the current output voltage | 50 | | `turn_hv_on()` / `turn_hv_off()` | Turns high voltage supply ON or OFF | 51 | | `get_hv_status()` | Returns current HV ON/OFF status | 52 | 53 | --- 54 | 55 | ### 12V Control 56 | 57 | | Method | Description | 58 | | ---------------------------------- | ----------------------------------------- | 59 | | `turn_12v_on()` / `turn_12v_off()` | Controls 12V auxiliary power | 60 | | `get_12v_status()` | Reads the ON/OFF status of the 12V supply | 61 | 62 | --- 63 | 64 | ### Temperature Monitoring 65 | 66 | | Method | Description | 67 | | -------------------- | -------------------------------------- | 68 | | `get_temperature1()` | Returns temperature from sensor 1 (°C) | 69 | | `get_temperature2()` | Returns temperature from sensor 2 (°C) | 70 | 71 | --- 72 | 73 | ### Fan Control 74 | 75 | | Method | Description | 76 | | ---------------------------------- | -------------------------------------------------------- | 77 | | `set_fan_speed(fan_id, fan_speed)` | Sets fan speed (0–100%) for fan ID 0 (bottom) or 1 (top) | 78 | | `get_fan_speed(fan_id)` | Reads current fan speed percentage | 79 | 80 | --- 81 | 82 | ### RGB LED Control 83 | 84 | | Method | Description | 85 | | -------------------- | ------------------------------------------- | 86 | | `set_rgb_led(state)` | Sets RGB LED: 0=OFF, 1=RED, 2=BLUE, 3=GREEN | 87 | | `get_rgb_led()` | Gets current RGB LED state | 88 | 89 | --- 90 | 91 | ### Advanced Control 92 | 93 | | Method | Description | 94 | | ------------------------------ | ----------------------------------------- | 95 | | `set_dacs(hvp, hvm, hrp, hrm)` | Sets DAC outputs for high voltage control | 96 | 97 | --- 98 | 99 | ## Notes 100 | 101 | - Most methods raise `ValueError` if UART is not connected. 102 | - All operations clear the UART buffer after use. 103 | - Demo mode behavior returns mocked values. 104 | 105 | --- 106 | 107 | ## Example 108 | 109 | ```python 110 | interface.hvcontroller.turn_12v_on() 111 | interface.hvcontroller.set_voltage(60.0) 112 | print(f"Output Voltage: {interface.hvcontroller.get_voltage()} V") 113 | interface.hvcontroller.turn_hv_on() 114 | ``` 115 | -------------------------------------------------------------------------------- /docs/includeme.rst: -------------------------------------------------------------------------------- 1 | Home 2 | ======== 3 | 4 | .. include:: ../README.rst 5 | :start-after: .. SPHINX-START 6 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to openlifu's documentation! 2 | ========================================= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | :caption: Contents: 7 | 8 | includeme 9 | api 10 | 11 | Indices and tables 12 | ================== 13 | 14 | * :ref:`genindex` 15 | * :ref:`modindex` 16 | * :ref:`search` 17 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/tx_device_api.md: -------------------------------------------------------------------------------- 1 | # TxDevice API Documentation 2 | 3 | The `TxDevice` class provides a high-level interface for communicating with and 4 | controlling ultrasound transmitter modules using the `LIFUUart` communication 5 | backend. 6 | 7 | --- 8 | 9 | ## Initialization 10 | 11 | ```python 12 | from openlifu.io.LIFUHVController import HVController 13 | from openlifu.io.LIFUUart import LIFUUart 14 | 15 | interface = LIFUInterface(TX_test_mode=False) 16 | tx_connected, hv_connected = interface.is_device_connected() 17 | if tx_connected and hv_connected: 18 | print("LIFU Device Fully connected.") 19 | else: 20 | print(f"LIFU Device NOT Fully Connected. TX: {tx_connected}, HV: {hv_connected}") 21 | 22 | if not tx_connected: 23 | print("TX Device not connected.") 24 | sys.exit() 25 | ``` 26 | 27 | --- 28 | 29 | ## Core Methods 30 | 31 | ### Device Communication 32 | 33 | | Method | Description | 34 | | ------------------- | -------------------------------------------------- | 35 | | `is_connected()` | Check if the TX device is connected | 36 | | `ping()` | Send a ping command to check connectivity | 37 | | `get_version()` | Get the firmware version (e.g., `v0.1.1`) | 38 | | `echo(data: bytes)` | Send and receive echo data to verify communication | 39 | | `toggle_led()` | Toggle the device onboard LED | 40 | | `get_hardware_id()` | Return the 16-byte hardware ID in hex format | 41 | | `soft_reset()` | Perform a software reset on the device | 42 | | `enter_dfu()` | Put the device into DFU mode | 43 | 44 | --- 45 | 46 | ### Trigger Configuration 47 | 48 | | Method | Description | 49 | | ------------------------------------ | ------------------------------------------- | 50 | | `set_trigger(...)` | Configure triggering with manual parameters | 51 | | `set_trigger_json(data: dict)` | Set trigger via JSON dictionary | 52 | | `get_trigger()` | Return current trigger config as a dict | 53 | | `get_trigger_json()` | Retrieve raw JSON trigger data | 54 | | `start_trigger()` / `stop_trigger()` | Begin or halt triggering | 55 | 56 | --- 57 | 58 | ### Register Operations 59 | 60 | | Method | Description | 61 | | --------------------------------------------- | -------------------------------- | 62 | | `write_register(identifier, addr, value)` | Write to a single register | 63 | | `write_register_verify(addr, value)` | Write and verify a register | 64 | | `read_register(addr)` | Read a register value | 65 | | `write_block(identifier, start_addr, values)` | Write a block of register values | 66 | | `write_block_verify(start_addr, values)` | Verified block write | 67 | 68 | --- 69 | 70 | ### Device Setup 71 | 72 | | Method | Description | 73 | | ---------------------------------------- | ----------------------------------------- | 74 | | `enum_tx7332_devices(num)` | Scan for TX7332 devices | 75 | | `demo_tx7332(identifier)` | Set test waveform to TX7332 | 76 | | `apply_all_registers()` | Apply all configured profiles to hardware | 77 | | `write_ti_config_to_tx_device(path, id)` | Load and apply config from TI text file | 78 | | `print` | Print TX interface info | 79 | 80 | --- 81 | 82 | ### Pulse/Delay Profiles 83 | 84 | | Method | Description | 85 | | ---------------------------------------------- | ----------------------------------- | 86 | | `set_solution(pulse, delays, apods, seq, ...)` | Apply full beamforming config | 87 | | `add_pulse_profile(profile)` | Add pulse shape settings | 88 | | `add_delay_profile(profile)` | Add delay+apodization configuration | 89 | 90 | --- 91 | 92 | ## Data Classes 93 | 94 | - `Tx7332PulseProfile`: Defines frequency, cycles, duty cycle, inversion, etc. 95 | - `Tx7332DelayProfile`: Defines delays and apodization for each channel 96 | - `TxDeviceRegisters`: Holds and manages TX chip register blocks per transmitter 97 | 98 | --- 99 | 100 | ## Example 101 | 102 | ```python 103 | pulse = {"frequency": 3e6, "duration": 2e-6} 104 | delays = [0] * 32 105 | apods = [1] * 32 106 | sequence = { 107 | "pulse_interval": 0.01, 108 | "pulse_count": 1, 109 | "pulse_train_interval": 0.0, 110 | "pulse_train_count": 1, 111 | } 112 | 113 | # Apply configuration 114 | if tx.is_connected(): 115 | tx.set_solution(pulse, delays, apods, sequence) 116 | tx.start_trigger() 117 | ``` 118 | 119 | --- 120 | 121 | ## Notes 122 | 123 | - Profile management assumes 32 transmit channels per chip. 124 | - Delay units default to seconds unless specified. 125 | - Device must be enumerated before applying register values. 126 | -------------------------------------------------------------------------------- /notebooks/README.md: -------------------------------------------------------------------------------- 1 | These notebooks demonstrate the functionality and usage of openlifu. 2 | 3 | # Using the jupytext notebooks 4 | 5 | The notebooks are in a [jupytext](https://jupytext.readthedocs.io/en/latest/) 6 | format. To use them, first install jupyter notebook and jupytext to the python 7 | environment in which openlifu was installed: 8 | 9 | ```sh 10 | pip install notebook jupytext 11 | ``` 12 | 13 | Then, either 14 | 15 | - launch a jupyter notebook server and choose to open the notebook `.py` files 16 | with jupytext (right click -> open with -> jupytext notebook), or 17 | - create paired `.ipynb` files to be used with the notebooks and then use the 18 | `.ipynb` files as normal: 19 | ```sh 20 | jupytext --to ipynb *.py 21 | ``` 22 | 23 | The paired `.ipynb` files will automatically be kept in sync with the `.py` 24 | files, so the `.py` files can be used in version control and the `.ipynb` files 25 | never need to be committed. 26 | -------------------------------------------------------------------------------- /notebooks/run_self_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from openlifu.io.LIFUInterface import LIFUInterface 4 | 5 | # set PYTHONPATH=%cd%\src;%PYTHONPATH% 6 | # python notebooks/run_self_test.py 7 | """ 8 | Test script to automate: 9 | 1. Connect to the device. 10 | 2. Test HVController: Turn HV on/off and check voltage. 11 | 3. Test Device functionality. 12 | """ 13 | print("Starting LIFU Test Script...") 14 | interface = LIFUInterface() 15 | tx_connected, hv_connected = interface.is_device_connected() 16 | if tx_connected and hv_connected: 17 | print("LIFU Device Fully connected.") 18 | else: 19 | print(f'LIFU Device NOT Fully Connected. TX: {tx_connected}, HV: {hv_connected}') 20 | 21 | print("Ping the device") 22 | interface.txdevice.ping() 23 | 24 | print("Run Self OneWire Test") 25 | interface.txdevice.run_test() 26 | 27 | print("Tests Finished") 28 | -------------------------------------------------------------------------------- /notebooks/stress_test.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import random 4 | 5 | from openlifu.io.LIFUInterface import LIFUInterface 6 | 7 | 8 | def run_test(interface, iterations): 9 | """ 10 | Run the LIFU test loop with random trigger settings. 11 | 12 | Args: 13 | interface (LIFUInterface): The LIFUInterface instance. 14 | iterations (int): Number of iterations to run. 15 | """ 16 | for i in range(iterations): 17 | print(f"Starting Test Iteration {i + 1}/{iterations}...") 18 | 19 | try: 20 | tx_connected, hv_connected = interface.is_device_connected() 21 | if not tx_connected: # or not hv_connected: 22 | raise ConnectionError(f"LIFU Device NOT Fully Connected. TX: {tx_connected}, HV: {hv_connected}") 23 | 24 | print("Ping the device") 25 | interface.txdevice.ping() 26 | 27 | print("Toggle LED") 28 | interface.txdevice.toggle_led() 29 | 30 | print("Get Version") 31 | version = interface.txdevice.get_version() 32 | print(f"Version: {version}") 33 | 34 | print("Echo Data") 35 | echo, length = interface.txdevice.echo(echo_data=b'Hello LIFU!') 36 | if length > 0: 37 | print(f"Echo: {echo.decode('utf-8')}") 38 | else: 39 | raise ValueError("Echo failed.") 40 | 41 | print("Get HW ID") 42 | hw_id = interface.txdevice.get_hardware_id() 43 | print(f"HWID: {hw_id}") 44 | 45 | print("Get Temperature") 46 | temperature = interface.txdevice.get_temperature() 47 | print(f"Temperature: {temperature} °C") 48 | 49 | print("Get Trigger") 50 | trigger_setting = interface.txdevice.get_trigger() 51 | if trigger_setting: 52 | print(f"Trigger Setting: {trigger_setting}") 53 | else: 54 | raise ValueError("Failed to get trigger setting.") 55 | 56 | print("Set Trigger with Random Parameters") 57 | # Generate random trigger frequency and pulse width 58 | trigger_frequency = random.randint(5, 25) # Random frequency between 5 and 25 Hz 59 | trigger_pulse_width = random.randint(10, 30) * 1000 # Random pulse width between 10 and 30 ms (convert to µs) 60 | 61 | json_trigger_data = { 62 | "TriggerFrequencyHz": trigger_frequency, 63 | "TriggerPulseCount": 0, 64 | "TriggerPulseWidthUsec": trigger_pulse_width, 65 | "TriggerPulseTrainInterval": 0, 66 | "TriggerPulseTrainCount": 0, 67 | "TriggerMode": 1, 68 | "ProfileIndex": 0, 69 | "ProfileIncrement": 0 70 | } 71 | trigger_setting = interface.txdevice.set_trigger_json(data=json_trigger_data) 72 | 73 | trigger_setting = interface.txdevice.get_trigger_json() 74 | if trigger_setting: 75 | print(f"Trigger Setting Applied: Frequency = {trigger_frequency} Hz, Pulse Width = {trigger_pulse_width // 1000} ms") 76 | if trigger_setting["TriggerFrequencyHz"] != trigger_frequency or trigger_setting["TriggerPulseWidthUsec"] != trigger_pulse_width: 77 | raise ValueError("Failed to set trigger setting.") 78 | else: 79 | raise ValueError("Failed to set trigger setting.") 80 | 81 | print(f"Iteration {i + 1} passed.\n") 82 | 83 | except Exception as e: 84 | print(f"Test failed on iteration {i + 1}: {e}") 85 | break 86 | 87 | if __name__ == "__main__": 88 | print("Starting LIFU Test Script...") 89 | interface = LIFUInterface() 90 | 91 | # Number of iterations to run 92 | test_iterations = 1000 # Change this to the desired number of iterations 93 | 94 | run_test(interface, test_iterations) 95 | -------------------------------------------------------------------------------- /notebooks/test_console.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | 5 | import base58 6 | 7 | from openlifu.io.LIFUInterface import LIFUInterface 8 | 9 | # set PYTHONPATH=%cd%\src;%PYTHONPATH% 10 | # python notebooks/test_console.py 11 | """ 12 | Test script to automate: 13 | 1. Connect to the device. 14 | 2. Test HVController: Turn HV on/off and check voltage. 15 | 3. Test Device functionality. 16 | """ 17 | print("Starting LIFU Test Script...") 18 | interface = LIFUInterface(TX_test_mode=False) 19 | tx_connected, hv_connected = interface.is_device_connected() 20 | if tx_connected and hv_connected: 21 | print("LIFU Device Fully connected.") 22 | else: 23 | print(f'LIFU Device NOT Fully Connected. TX: {tx_connected}, HV: {hv_connected}') 24 | 25 | if not hv_connected: 26 | print("HV Controller not connected.") 27 | sys.exit() 28 | 29 | print("Ping the device") 30 | interface.hvcontroller.ping() 31 | 32 | print("Toggle LED") 33 | interface.hvcontroller.toggle_led() 34 | 35 | print("Get Version") 36 | version = interface.hvcontroller.get_version() 37 | print(f"Version: {version}") 38 | 39 | print("Echo Data") 40 | echo, echo_len = interface.hvcontroller.echo(echo_data=b'Hello LIFU!') 41 | if echo_len > 0: 42 | print(f"Echo: {echo.decode('utf-8')}") # Echo: Hello LIFU! 43 | else: 44 | print("Echo failed.") 45 | 46 | print("Get HW ID") 47 | hw_id = interface.hvcontroller.get_hardware_id() 48 | print(f"HW ID: {hw_id}") 49 | encoded_id = base58.b58encode(bytes.fromhex(hw_id)).decode() 50 | print(f"OW-LIFU-CON-{encoded_id}") 51 | 52 | print("Get Temperature1") 53 | temp1 = interface.hvcontroller.get_temperature1() 54 | print(f"Temperature1: {temp1}") 55 | 56 | print("Get Temperature2") 57 | temp2 = interface.hvcontroller.get_temperature2() 58 | print(f"Temperature2: {temp2}") 59 | 60 | print("Set Bottom Fan Speed to 20%") 61 | btfan_speed = interface.hvcontroller.set_fan_speed(fan_id=0, fan_speed=20) 62 | print(f"Bottom Fan Speed: {btfan_speed}") 63 | 64 | print("Set Top Fan Speed to 40%") 65 | tpfan_speed = interface.hvcontroller.set_fan_speed(fan_id=1, fan_speed=40) 66 | print(f"Bottom Fan Speed: {tpfan_speed}") 67 | 68 | print("Get Bottom Fan Speed") 69 | btfan_speed = interface.hvcontroller.get_fan_speed(fan_id=0) 70 | print(f"Bottom Fan Speed: {btfan_speed}") 71 | 72 | print("Get Top Fan Speed") 73 | tpfan_speed = interface.hvcontroller.get_fan_speed(fan_id=1) 74 | print(f"Bottom Fan Speed: {tpfan_speed}") 75 | 76 | print("Set RGB LED") 77 | rgb_led = interface.hvcontroller.set_rgb_led(rgb_state=2) 78 | print(f"RGB STATE: {rgb_led}") 79 | 80 | print("Get RGB LED") 81 | rgb_led_state = interface.hvcontroller.get_rgb_led() 82 | print(f"RGB STATE: {rgb_led_state}") 83 | 84 | print("Test 12V...") 85 | if interface.hvcontroller.turn_12v_on(): 86 | print("12V ON Press enter to TURN OFF:") 87 | input() # Wait for the user to press Enter 88 | if interface.hvcontroller.turn_12v_off(): 89 | print("12V OFF.") 90 | else: 91 | print("Failed to turn off 12V") 92 | else: 93 | print("Failed to turn on 12V.") 94 | 95 | # Set High Voltage Level 96 | print("Set HV Power to +/- 85V") 97 | if interface.hvcontroller.set_voltage(voltage=75.0): 98 | print("Voltage set to 85.0 V.") 99 | else: 100 | print("Failed to set voltage.") 101 | 102 | # Get Set High Voltage Setting 103 | print("Get Current HV Voltage") 104 | read_voltage = interface.hvcontroller.get_voltage() 105 | print(f"HV Voltage {read_voltage} V.") 106 | 107 | 108 | print("Test HV Supply...") 109 | if interface.hvcontroller.turn_hv_on(): 110 | # Get Set High Voltage Setting 111 | read_voltage = interface.hvcontroller.get_voltage() 112 | print(f"HV Voltage {read_voltage} V.") 113 | print("HV ON Press enter to TURN OFF:") 114 | input() # Wait for the user to press Enter 115 | if interface.hvcontroller.turn_hv_off(): 116 | print("HV OFF.") 117 | else: 118 | print("Failed to turn off HV") 119 | else: 120 | print("Failed to turn on HV.") 121 | 122 | print("Reset DevConsoleice:") 123 | # Ask the user for confirmation 124 | user_input = input("Do you want to reset the Console? (y/n): ").strip().lower() 125 | 126 | if user_input == 'y': 127 | if interface.hvcontroller.soft_reset(): 128 | print("Reset Successful.") 129 | elif user_input == 'n': 130 | print("Reset canceled.") 131 | else: 132 | print("Invalid input. Please enter 'y' or 'n'.") 133 | -------------------------------------------------------------------------------- /notebooks/test_console2.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | 5 | from openlifu.io.LIFUInterface import LIFUInterface 6 | 7 | # set PYTHONPATH=%cd%\src;%PYTHONPATH% 8 | # python notebooks/test_console.py 9 | """ 10 | Test script to automate: 11 | 1. Connect to the device. 12 | 2. Test HVController: Turn HV on/off and check voltage. 13 | 3. Test Device functionality. 14 | """ 15 | print("Starting LIFU Test Script...") 16 | interface = LIFUInterface(TX_test_mode=False) 17 | tx_connected, hv_connected = interface.is_device_connected() 18 | if tx_connected and hv_connected: 19 | print("LIFU Device Fully connected.") 20 | else: 21 | print(f'LIFU Device NOT Fully Connected. TX: {tx_connected}, HV: {hv_connected}') 22 | 23 | if not hv_connected: 24 | print("HV Controller not connected.") 25 | sys.exit() 26 | 27 | print("Ping the device") 28 | interface.hvcontroller.ping() 29 | 30 | # Set High Voltage Level 31 | if not interface.hvcontroller.set_voltage(voltage=30.0): 32 | print("Failed to set voltage.") 33 | 34 | # Get Set High Voltage Setting 35 | print("Get Current HV Voltage") 36 | read_voltage = interface.hvcontroller.get_voltage() 37 | print(f"HV Voltage {read_voltage} V.") 38 | 39 | 40 | print("Test HV Supply...") 41 | if interface.hvcontroller.turn_hv_on(): 42 | # Get Set High Voltage Setting 43 | read_voltage = interface.hvcontroller.get_voltage() 44 | print(f"HV Voltage {read_voltage} V.") 45 | print("HV ON Press enter to TURN OFF:") 46 | input() # Wait for the user to press Enter 47 | if interface.hvcontroller.turn_hv_off(): 48 | print("HV OFF.") 49 | else: 50 | print("Failed to turn off HV") 51 | else: 52 | print("Failed to turn on HV.") 53 | 54 | print("Reset DevConsoleice:") 55 | # Ask the user for confirmation 56 | user_input = input("Do you want to reset the Console? (y/n): ").strip().lower() 57 | 58 | if user_input == 'y': 59 | if interface.hvcontroller.soft_reset(): 60 | print("Reset Successful.") 61 | elif user_input == 'n': 62 | print("Reset canceled.") 63 | else: 64 | print("Invalid input. Please enter 'y' or 'n'.") 65 | -------------------------------------------------------------------------------- /notebooks/test_console_dfu.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | 5 | from openlifu.io.LIFUInterface import LIFUInterface 6 | 7 | # set PYTHONPATH=%cd%\src;%PYTHONPATH% 8 | # python notebooks/test_update_firmware.py 9 | """ 10 | Test script to automate: 11 | 1. Connect to the device. 12 | 2. Test HVController: Turn HV on/off and check voltage. 13 | 3. Test Device functionality. 14 | """ 15 | print("Starting LIFU Test Script...") 16 | interface = LIFUInterface(TX_test_mode=False) 17 | tx_connected, hv_connected = interface.is_device_connected() 18 | if tx_connected and hv_connected: 19 | print("LIFU Device Fully connected.") 20 | else: 21 | print(f'LIFU Device NOT Fully Connected. TX: {tx_connected}, HV: {hv_connected}') 22 | 23 | if not hv_connected: 24 | print("HV Controller not connected.") 25 | sys.exit(1) 26 | 27 | print("Ping the device") 28 | interface.hvcontroller.ping() 29 | 30 | print("Enter DFU mode") 31 | interface.hvcontroller.enter_dfu() 32 | 33 | print("Use stm32 cube programmer to update firmware, power cycle will put the console back into an operating state") 34 | sys.exit(0) 35 | -------------------------------------------------------------------------------- /notebooks/test_multiple_modules.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from openlifu.io.LIFUInterface import LIFUInterface 4 | 5 | # set PYTHONPATH=%cd%\src;%PYTHONPATH% 6 | # python notebooks/test_multiple_modules.py 7 | """ 8 | Test script to automate: 9 | 1. Connect to the device. 10 | 2. Test HVController: Turn HV on/off and check voltage. 11 | 3. Test Device functionality. 12 | """ 13 | print("Starting LIFU Test Script...") 14 | interface = LIFUInterface() 15 | tx_connected, hv_connected = interface.is_device_connected() 16 | if tx_connected and hv_connected: 17 | print("LIFU Device Fully connected.") 18 | else: 19 | print(f'LIFU Device NOT Fully Connected. TX: {tx_connected}, HV: {hv_connected}') 20 | 21 | print("Ping the device") 22 | interface.txdevice.ping() 23 | 24 | print("Enumerate TX7332 chips") 25 | num_tx_devices = interface.txdevice.enum_tx7332_devices() 26 | if num_tx_devices > 0: 27 | print(f"Number of TX7332 devices found: {num_tx_devices}") 28 | 29 | print("Write Demo Registers to TX7332 chips") 30 | for device_index in range(num_tx_devices): 31 | interface.txdevice.demo_tx7332(device_index) 32 | 33 | print("Starting Trigger...") 34 | if interface.txdevice.start_trigger(): 35 | print("Trigger Running Press enter to STOP:") 36 | input() # Wait for the user to press Enter 37 | if interface.txdevice.stop_trigger(): 38 | print("Trigger stopped successfully.") 39 | else: 40 | print("Failed to stop trigger.") 41 | else: 42 | print("Failed to start trigger.") 43 | 44 | else: 45 | raise Exception("No TX7332 devices found.") 46 | -------------------------------------------------------------------------------- /notebooks/test_nifti.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # formats: ipynb,py:percent 5 | # text_representation: 6 | # extension: .py 7 | # format_name: percent 8 | # format_version: '1.3' 9 | # jupytext_version: 1.16.2 10 | # kernelspec: 11 | # display_name: Python 3 (ipykernel) 12 | # language: python 13 | # name: python3 14 | # --- 15 | 16 | # %% 17 | from __future__ import annotations 18 | 19 | modified_kwave_path = R'C:\Users\pjh7\git\k-wave-python' 20 | slicer_exe = R"C:\Users\pjh7\AppData\Local\NA-MIC\Slicer 5.2.2\Slicer.exe" 21 | import sys 22 | 23 | sys.path.append(modified_kwave_path) 24 | import logging 25 | 26 | import openlifu 27 | 28 | root = logging.getLogger() 29 | loglevel = logging.INFO 30 | root.setLevel(loglevel) 31 | handler = logging.StreamHandler(sys.stdout) 32 | handler.setLevel(loglevel) 33 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 34 | handler.setFormatter(formatter) 35 | root.addHandler(handler) 36 | import numpy as np 37 | 38 | # %% 39 | 40 | arr = openlifu.Transducer.gen_matrix_array(nx=8, ny=8, pitch=4, kerf=.5, units="mm", impulse_response=1e5) 41 | trans_matrix = np.array( 42 | [[-1, 0, 0, 0], 43 | [0, .05, np.sqrt(1-.05**2), -105], 44 | [0, np.sqrt(1-.05**2), -.05, 5], 45 | [0, 0, 0, 1]]) 46 | arr.rescale("mm") 47 | arr.matrix = trans_matrix 48 | pt = openlifu.Point(position=(5,-60,-8), units="mm", radius=2) 49 | 50 | # %% 51 | pulse = openlifu.Pulse(frequency=400e3, duration=3/400e3) 52 | sequence = openlifu.Sequence() 53 | focal_pattern = openlifu.focal_patterns.Wheel(center=True, spoke_radius=5, num_spokes=5) 54 | sim_setup = openlifu.SimSetup(dt=2e-7, t_end=100e-6) 55 | protocol = openlifu.Protocol( 56 | pulse=pulse, 57 | sequence=sequence, 58 | focal_pattern=focal_pattern, 59 | sim_setup=sim_setup) 60 | pts = protocol.focal_pattern.get_targets(pt) 61 | coords = protocol.sim_setup.get_coords() 62 | params = protocol.seg_method.ref_params(coords) 63 | delays, apod = protocol.beamform(arr=arr, target=pts[0], params=params) 64 | 65 | 66 | # %% 67 | ds = openlifu.sim.run_simulation(arr=arr, 68 | params=params, 69 | delays=delays, 70 | apod= apod, 71 | freq = pulse.frequency, 72 | cycles = np.max([np.round(pulse.duration * pulse.frequency), 20]), 73 | dt=protocol.sim_setup.dt, 74 | t_end=protocol.sim_setup.t_end, 75 | amplitude = 1) 76 | 77 | # %% 78 | ds['p_max'].sel(lat=-5).plot.imshow() 79 | 80 | # %% 81 | # Export to .nii.gz 82 | import nibabel as nb 83 | 84 | output_filename = "foo.nii.gz" 85 | trans_matrix = np.array( 86 | [[-1, 0, 0, 0], 87 | [0, .05, np.sqrt(1-.05**2), -105], 88 | [0, np.sqrt(1-.05**2), -.05, 5], 89 | [0, 0, 0, 1]]) 90 | da = ds['p_max'].interp({'lat':np.arange(-30, 30.1, 0.5),'ele':np.arange(-30, 30.1, 0.5), 'ax': np.arange(-4,70.1,0.5)}) 91 | origin_local = [float(val[0]) for dim, val in da.coords.items()] 92 | dx = [float(val[1]-val[0]) for dim, val in da.coords.items()] 93 | affine = np.array([-1,-1,1,1]).reshape(4,1)*np.concatenate([trans_matrix[:,:3], trans_matrix @ np.array([*origin_local, 1]).reshape([4,1])], axis=1)*np.array([*dx, 1]).reshape([1,4]) 94 | data = da.data 95 | im = nb.Nifti1Image(data, affine) 96 | h = im.header 97 | h.set_xyzt_units('mm', 'sec') 98 | im = nb.as_closest_canonical(im) 99 | im.to_filename(output_filename) 100 | 101 | 102 | # %% 103 | # Load into Slicer 104 | import slicerio.server 105 | 106 | slicerio.server.file_load(output_filename, slicer_executable=slicer_exe) 107 | -------------------------------------------------------------------------------- /notebooks/test_nucleo.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from openlifu.io.LIFUInterface import LIFUInterface 4 | 5 | # set PYTHONPATH=%cd%\src;%PYTHONPATH% 6 | # python notebooks/test_nucleo.py 7 | """ 8 | Test script to automate: 9 | 1. Connect to the device. 10 | 2. Test HVController: Turn HV on/off and check voltage. 11 | 3. Test Device functionality. 12 | """ 13 | print("Starting LIFU Test Script...") 14 | interface = LIFUInterface() 15 | tx_connected, hv_connected = interface.is_device_connected() 16 | if tx_connected and hv_connected: 17 | print("LIFU Device Fully connected.") 18 | else: 19 | print(f'LIFU Device NOT Fully Connected. TX: {tx_connected}, HV: {hv_connected}') 20 | 21 | print("Ping the device") 22 | interface.txdevice.ping() 23 | 24 | print("Toggle LED") 25 | interface.txdevice.toggle_led() 26 | 27 | print("Get Version") 28 | version = interface.txdevice.get_version() 29 | print(f"Version: {version}") 30 | 31 | print("Echo Data") 32 | echo, echo_len = interface.txdevice.echo(echo_data=b'Hello LIFU!') 33 | if echo_len > 0: 34 | print(f"Echo: {echo.decode('utf-8')}") # Echo: Hello LIFU! 35 | else: 36 | print("Echo failed.") 37 | 38 | print("Get HW ID") 39 | hw_id = interface.txdevice.get_hardware_id() 40 | print(f"HWID: {hw_id}") 41 | 42 | print("Get Temperature") 43 | temperature = interface.txdevice.get_temperature() 44 | print(f"Temperature: {temperature} °C") 45 | 46 | print("Get Ambient") 47 | temperature = interface.txdevice.get_ambient_temperature() 48 | print(f"Ambient Temperature: {temperature} °C") 49 | 50 | print("Get Trigger") 51 | current_trigger_setting = interface.txdevice.get_trigger_json() 52 | if current_trigger_setting: 53 | print(f"Current Trigger Setting: {current_trigger_setting}") 54 | else: 55 | print("Failed to get current trigger setting.") 56 | 57 | print("Starting Trigger with current setting...") 58 | if interface.txdevice.start_trigger(): 59 | print("Trigger Running Press enter to STOP:") 60 | input() # Wait for the user to press Enter 61 | if interface.txdevice.stop_trigger(): 62 | print("Trigger stopped successfully.") 63 | else: 64 | print("Failed to stop trigger.") 65 | else: 66 | print("Failed to get trigger setting.") 67 | 68 | print("Set Trigger") 69 | json_trigger_data = { 70 | "TriggerFrequencyHz": 25, 71 | "TriggerPulseCount": 0, 72 | "TriggerPulseWidthUsec": 20000, 73 | "TriggerPulseTrainInterval": 0, 74 | "TriggerPulseTrainCount": 0, 75 | "TriggerMode": 1, 76 | "ProfileIndex": 0, 77 | "ProfileIncrement": 0 78 | } 79 | 80 | trigger_setting = interface.txdevice.set_trigger_json(data=json_trigger_data) 81 | if trigger_setting: 82 | print(f"Trigger Setting: {trigger_setting}") 83 | else: 84 | print("Failed to set trigger setting.") 85 | 86 | print("Starting Trigger with updated setting...") 87 | if interface.txdevice.start_trigger(): 88 | print("Trigger Running Press enter to STOP:") 89 | input() # Wait for the user to press Enter 90 | if interface.txdevice.stop_trigger(): 91 | print("Trigger stopped successfully.") 92 | else: 93 | print("Failed to stop trigger.") 94 | else: 95 | print("Failed to get trigger setting.") 96 | 97 | print("Reset Device:") 98 | # Ask the user for confirmation 99 | user_input = input("Do you want to reset the device? (y/n): ").strip().lower() 100 | 101 | if user_input == 'y': 102 | if interface.txdevice.soft_reset(): 103 | print("Reset Successful.") 104 | elif user_input == 'n': 105 | print("Reset canceled.") 106 | else: 107 | print("Invalid input. Please enter 'y' or 'n'.") 108 | 109 | print("Update Device:") 110 | # Ask the user for confirmation 111 | user_input = input("Do you want to update the device? (y/n): ").strip().lower() 112 | 113 | if user_input == 'y': 114 | if interface.txdevice.enter_dfu(): 115 | print("Entering DFU Mode.") 116 | elif user_input == 'n': 117 | print("Update canceled.") 118 | else: 119 | print("Invalid input. Please enter 'y' or 'n'.") 120 | -------------------------------------------------------------------------------- /notebooks/test_pwr.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | import time 5 | 6 | from openlifu.io.LIFUInterface import LIFUInterface 7 | 8 | # set PYTHONPATH=%cd%\src;%PYTHONPATH% 9 | # python notebooks/test_pwr.py 10 | 11 | print("Starting LIFU Test Script...") 12 | 13 | interface = LIFUInterface() 14 | tx_connected, hv_connected = interface.is_device_connected() 15 | 16 | if not tx_connected: 17 | print("TX device not connected. Attempting to turn on 12V...") 18 | interface.hvcontroller.turn_12v_on() 19 | 20 | # Give time for the TX device to power up and enumerate over USB 21 | time.sleep(2) 22 | 23 | # Cleanup and recreate interface to reinitialize USB devices 24 | interface.stop_monitoring() 25 | del interface 26 | time.sleep(1) # Short delay before recreating 27 | 28 | print("Reinitializing LIFU interface after powering 12V...") 29 | interface = LIFUInterface() 30 | 31 | # Re-check connection 32 | tx_connected, hv_connected = interface.is_device_connected() 33 | 34 | if tx_connected and hv_connected: 35 | print("✅ LIFU Device fully connected.") 36 | else: 37 | print("❌ LIFU Device NOT fully connected.") 38 | print(f" TX Connected: {tx_connected}") 39 | print(f" HV Connected: {hv_connected}") 40 | sys.exit(1) 41 | 42 | print("Ping the device") 43 | interface.hvcontroller.ping() 44 | 45 | print("Starting DAC value increments...") 46 | hvp_value = 2400 47 | hrp_value = 2095 48 | hrm_value = 2095 49 | hvm_value = 2400 50 | 51 | print(f"Setting DACs: hvp={hvp_value}, hrp={hrp_value}, hvm={hvm_value}, hrm={hrm_value}") 52 | if interface.hvcontroller.set_dacs(hvp=hvp_value, hrp=hrp_value, hvm=hvm_value, hrm=hrm_value): 53 | print(f"DACs successfully set to hvp={hvp_value}, hrp={hrp_value}, hvm={hvm_value}, hrm={hrm_value}") 54 | else: 55 | print("Failed to set DACs.") 56 | 57 | print("Test HV Supply...") 58 | print("HV OFF. Press enter to TURN ON:") 59 | input() # Wait for user input 60 | if interface.hvcontroller.turn_hv_on(): 61 | print("HV ON. Press enter to TURN OFF:") 62 | input() # Wait for user input 63 | if interface.hvcontroller.turn_hv_off(): 64 | print("HV OFF.") 65 | else: 66 | print("Failed to turn off HV.") 67 | else: 68 | print("Failed to turn on HV.") 69 | -------------------------------------------------------------------------------- /notebooks/test_registers.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # formats: ipynb,py:percent 5 | # text_representation: 6 | # extension: .py 7 | # format_name: percent 8 | # format_version: '1.3' 9 | # jupytext_version: 1.16.4 10 | # kernelspec: 11 | # display_name: env 12 | # language: python 13 | # name: python3 14 | # --- 15 | 16 | # %% 17 | from __future__ import annotations 18 | 19 | import numpy as np 20 | 21 | from openlifu.io.LIFUTXDevice import ( 22 | Tx7332DelayProfile, 23 | Tx7332PulseProfile, 24 | Tx7332Registers, 25 | TxDeviceRegisters, 26 | print_regs, 27 | ) 28 | 29 | # %% 30 | tx = Tx7332Registers() 31 | delays = np.arange(32)*1e-6 32 | apodizations = np.ones(32) 33 | delay_profile_1 = Tx7332DelayProfile(1, delays, apodizations) 34 | tx.add_delay_profile(delay_profile_1) 35 | print('CONTROL') 36 | print_regs(tx.get_delay_control_registers()) 37 | print('DATA') 38 | print_regs(tx.get_delay_data_registers()) 39 | 40 | # %% 41 | frequency = 150e3 42 | cycles = 3 43 | pulse_profile_1 = Tx7332PulseProfile(1, frequency, cycles) 44 | print('CONTROL') 45 | tx.add_pulse_profile(pulse_profile_1) 46 | print_regs(tx.get_pulse_control_registers()) 47 | print('DATA') 48 | print_regs(tx.get_pulse_data_registers()) 49 | 50 | # %% 51 | tx.add_delay_profile(delay_profile_1) 52 | tx.add_pulse_profile(pulse_profile_1) 53 | r = tx.get_registers(profiles="all") 54 | print_regs(r) 55 | 56 | # %% 57 | x = np.linspace(-0.5, 0.5, 32)*4e-2 58 | r = np.sqrt(x**2 + 5e-2**2) 59 | delays = (r.max()-r)/1500 60 | apodizations = [0,1]*16 61 | delay_profile_2 = Tx7332DelayProfile(2, delays, apodizations) 62 | tx.add_delay_profile(delay_profile_2) 63 | print(f'{len(tx._delay_profiles_list)} Delay Profiles') 64 | print(f'{len(tx._pulse_profiles_list)} Pulse Profiles') 65 | print('') 66 | for pack in [False, True]: 67 | print(f'Pack: {pack}') 68 | for profile_opts in ["active", "configured", "all"]: 69 | r = tx.get_registers(profiles=profile_opts, pack=pack) 70 | print(f'{profile_opts}: {len(r)} Writes') 71 | print('') 72 | 73 | # %% 74 | for index in [1,2]: 75 | tx.activate_delay_profile(index) 76 | print('\n') 77 | print(index) 78 | print('CONTROL') 79 | print_regs(tx.get_delay_control_registers()) 80 | print('DATA') 81 | print_regs(tx.get_delay_data_registers()) 82 | 83 | 84 | # %% 85 | txm = TxDeviceRegisters() 86 | delays = np.arange(64)*1e-6 87 | apodizations = np.ones(64) 88 | module_delay_profile_1 = Tx7332DelayProfile(1, delays, apodizations) 89 | txm.add_delay_profile(module_delay_profile_1) 90 | txm.add_pulse_profile(pulse_profile_1) 91 | for i, r in enumerate(txm.get_delay_control_registers()): 92 | print(f'CONTROL {i}') 93 | print_regs(r) 94 | for i, r in enumerate(txm.get_delay_data_registers()): 95 | print(f'DATA {i}') 96 | print_regs(r) 97 | 98 | # %% 99 | module_delay_profile_1 100 | 101 | # %% 102 | x = np.linspace(-0.5, 0.5, 64)*4e-2 103 | r = np.sqrt(x**2 + 5e-2**2) 104 | delays = (r.max()-r)/1500 105 | apodizations = [0,1]*32 106 | module_delay_profile_2 = Tx7332DelayProfile(2, delays, apodizations) 107 | frequency = 100e3 108 | cycles = 200 109 | pulse_profile_2 = Tx7332PulseProfile(2, frequency, cycles) 110 | txm.add_delay_profile(module_delay_profile_2, activate=True) 111 | txm.add_pulse_profile(pulse_profile_2, activate=True) 112 | for i, r in enumerate(txm.get_delay_control_registers()): 113 | print(f'DELAY CONTROL {i}') 114 | print_regs(r) 115 | for i, r in enumerate(txm.get_delay_data_registers()): 116 | print(f'DELAY DATA {i}') 117 | print_regs(r) 118 | 119 | # %% 120 | for i, r in enumerate(txm.get_pulse_control_registers()): 121 | print(f'PULSE CONTROL {i}') 122 | print_regs(r) 123 | for i, r in enumerate(txm.get_pulse_data_registers()): 124 | print(f'PULSE DATA {i}') 125 | print_regs(r) 126 | 127 | # %% 128 | r = {'DELAY CONTROL': {}, 'DELAY DATA': {}, 'PULSE CONTROL': {}, 'PULSE DATA': {}} 129 | rtemplate = {} 130 | profiles = [1,2] 131 | for index in profiles: 132 | rtemplate[index] = ['---------']*2 133 | for index in profiles: 134 | txm.activate_delay_profile(index) 135 | txm.activate_pulse_profile(index) 136 | rdcm = txm.get_delay_control_registers() 137 | rddm = txm.get_delay_data_registers() 138 | rpcm = txm.get_pulse_control_registers() 139 | rpdm = txm.get_pulse_data_registers() 140 | d = {'DELAY CONTROL': rdcm, 'DELAY DATA': rddm, 'PULSE CONTROL': rpcm, 'PULSE DATA': rpdm} 141 | for k, rm in d.items(): 142 | for txi, rc in enumerate(rm): 143 | for addr, value in rc.items(): 144 | if addr not in r[k]: 145 | r[k][addr] = {i: ['---------']*2 for i in profiles} 146 | r[k][addr][index][txi] = f'x{value:08x}' 147 | h = [f'{"Profile " + str(i):19s}' for i in profiles] 148 | print(f" {' | '.join(h)}") 149 | h1 = [f'{"TX" + str(i):>9s}' for i in [0,1]] 150 | h1s = [' '.join(h1)]*len(profiles) 151 | print(f"addr: {' | '.join(h1s)}") 152 | for k, rm in r.items(): 153 | print(f"{k}") 154 | for addr, rr in rm.items(): 155 | print(f"x{addr:03x}: {' | '.join([' '.join(rr[i]) for i in profiles])}") 156 | 157 | # %% 158 | txm.get_registers(pack=False, pack_single=False) 159 | -------------------------------------------------------------------------------- /notebooks/test_reset.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from openlifu.io.LIFUInterface import LIFUInterface 4 | 5 | # set PYTHONPATH=%cd%\src;%PYTHONPATH% 6 | # python notebooks/test_reset.py 7 | 8 | print("Starting LIFU Test Script...") 9 | interface = LIFUInterface() 10 | tx_connected, hv_connected = interface.is_device_connected() 11 | if tx_connected and hv_connected: 12 | print("LIFU Device Fully connected.") 13 | else: 14 | print(f'LIFU Device NOT Fully Connected. TX: {tx_connected}, HV: {hv_connected}') 15 | 16 | print("Reset Device:") 17 | # Ask the user for confirmation 18 | user_input = input("Do you want to reset the device? (y/n): ").strip().lower() 19 | 20 | if user_input == 'y': 21 | if interface.txdevice.soft_reset(): 22 | print("Reset Successful.") 23 | elif user_input == 'n': 24 | print("Reset canceled.") 25 | else: 26 | print("Invalid input. Please enter 'y' or 'n'.") 27 | -------------------------------------------------------------------------------- /notebooks/test_solution.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # text_representation: 5 | # extension: .py 6 | # format_name: light 7 | # format_version: '1.5' 8 | # jupytext_version: 1.16.4 9 | # kernelspec: 10 | # display_name: env 11 | # language: python 12 | # name: python3 13 | # --- 14 | from __future__ import annotations 15 | 16 | # + 17 | import numpy as np 18 | 19 | import openlifu 20 | 21 | # - 22 | 23 | pulse = openlifu.Pulse(frequency=500e3, duration=2e-5) 24 | pt = openlifu.Point(position=(0,0,30), units="mm") 25 | sequence = openlifu.Sequence( 26 | pulse_interval=0.1, 27 | pulse_count=10, 28 | pulse_train_interval=1, 29 | pulse_train_count=1 30 | ) 31 | solution = openlifu.Solution( 32 | id="solution", 33 | name="Solution", 34 | protocol_id="example_protocol", 35 | transducer_id="example_transducer", 36 | delays = np.zeros((1,64)), 37 | apodizations = np.ones((1,64)), 38 | pulse = pulse, 39 | voltage=1.0, 40 | sequence = sequence, 41 | target=pt, 42 | foci=[pt], 43 | approved=True 44 | ) 45 | 46 | 47 | solution 48 | 49 | ifx = openlifu.LIFUInterface() 50 | 51 | ifx.set_solution(solution.to_dict()) 52 | 53 | txm = ifx.txdevice.tx_registers 54 | r = {'DELAY CONTROL': {}, 'DELAY DATA': {}, 'PULSE CONTROL': {}, 'PULSE DATA': {}} 55 | rtemplate = {} 56 | profiles = ifx.txdevice.tx_registers.configured_pulse_profiles() 57 | for index in profiles: 58 | rtemplate[index] = ['---------']*2 59 | for index in profiles: 60 | txm.activate_delay_profile(index) 61 | txm.activate_pulse_profile(index) 62 | rdcm = txm.get_delay_control_registers() 63 | rddm = txm.get_delay_data_registers() 64 | rpcm = txm.get_pulse_control_registers() 65 | rpdm = txm.get_pulse_data_registers() 66 | d = {'DELAY CONTROL': rdcm, 'DELAY DATA': rddm, 'PULSE CONTROL': rpcm, 'PULSE DATA': rpdm} 67 | for k, rm in d.items(): 68 | for txi, rc in enumerate(rm): 69 | for addr, value in rc.items(): 70 | if addr not in r[k]: 71 | r[k][addr] = {i: ['---------']*2 for i in profiles} 72 | r[k][addr][index][txi] = f'x{value:08x}' 73 | h = [f'{"Profile " + str(i):19s}' for i in profiles] 74 | print(f" {' | '.join(h)}") 75 | h1 = [f'{"TX" + str(i):>9s}' for i in [0,1]] 76 | h1s = [' '.join(h1)]*len(profiles) 77 | print(f"addr: {' | '.join(h1s)}") 78 | for k, rm in r.items(): 79 | print(f"{k}") 80 | for addr, rr in rm.items(): 81 | print(f"x{addr:03x}: {' | '.join([' '.join(rr[i]) for i in profiles])}") 82 | -------------------------------------------------------------------------------- /notebooks/test_solution_analysis.py: -------------------------------------------------------------------------------- 1 | # --- 2 | # jupyter: 3 | # jupytext: 4 | # text_representation: 5 | # extension: .py 6 | # format_name: light 7 | # format_version: '1.5' 8 | # jupytext_version: 1.16.4 9 | # kernelspec: 10 | # display_name: env 11 | # language: python 12 | # name: python3 13 | # --- 14 | from __future__ import annotations 15 | 16 | import numpy as np 17 | 18 | from openlifu.bf import Pulse, Sequence, apod_methods, focal_patterns 19 | from openlifu.geo import Point 20 | from openlifu.plan import Protocol 21 | from openlifu.plan.param_constraint import ParameterConstraint 22 | from openlifu.sim import SimSetup 23 | from openlifu.xdc import Transducer 24 | 25 | # + 26 | f0 = 400e3 27 | pulse = Pulse(frequency=f0, duration=10/f0) 28 | sequence = Sequence(pulse_interval=0.1, pulse_count=9, pulse_train_interval=0, pulse_train_count=1) 29 | focal_pattern = focal_patterns.SinglePoint(target_pressure=1.2e6) 30 | focal_pattern = focal_patterns.Wheel(center=False, spoke_radius=5, num_spokes=3, target_pressure=1.2e6) 31 | apod_method = apod_methods.MaxAngle(30) 32 | sim_setup = SimSetup(x_extent=(-30,30), y_extent=(-30,30), z_extent=(-4,70)) 33 | protocol = Protocol( 34 | id='test_protocol', 35 | name='Test Protocol', 36 | pulse=pulse, 37 | sequence=sequence, 38 | focal_pattern=focal_pattern, 39 | apod_method=apod_method, 40 | sim_setup=sim_setup) 41 | 42 | target = Point(position=np.array([0, 0, 50]), units="mm", radius=2) 43 | trans = Transducer.gen_matrix_array(nx=8, ny=8, pitch=4, kerf=0.5, id="m3", name="openlifu", impulse_response=1e6/10) 44 | # - 45 | 46 | solution, sim_res, scaled_analysis = protocol.calc_solution( 47 | target=target, 48 | transducer=trans, 49 | simulate=True, 50 | scale=True) 51 | 52 | pc = {"MI":ParameterConstraint('<', 1.8, 1.85), "TIC":ParameterConstraint('<', 2.0), 'global_isppa_Wcm2':ParameterConstraint('within', error_value=(49, 190))} 53 | if scaled_analysis is not None: 54 | scaled_analysis.to_table(constraints=pc).set_index('Param')[['Value', 'Units', 'Status']] 55 | 56 | protocol = Protocol.from_file('../tests/resources/example_db/protocols/example_protocol/example_protocol.json') 57 | solution, sim_res, analysis = protocol.calc_solution( 58 | target=target, 59 | transducer=trans, 60 | simulate=True, 61 | scale=True) 62 | if analysis is not None: 63 | analysis.to_table().set_index('Param')[['Value', 'Units', 'Status']] 64 | -------------------------------------------------------------------------------- /notebooks/test_temp.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import struct 5 | import time 6 | 7 | from openlifu.io.core import UART, UartPacket 8 | from openlifu.io.ctrl_if import CTRL_IF 9 | from openlifu.io.utils import format_and_print_hex, list_vcp_with_vid_pid 10 | 11 | 12 | async def main(): 13 | # Select communication port 14 | 15 | # s = UART('COM9', timeout=5) 16 | # s = UART('COM31', timeout=5) 17 | #s = UART('COM16', timeout=5) 18 | CTRL_BOARD = True # change to false and specify PORT_NAME for Nucleo Board 19 | PORT_NAME = "COM16" 20 | s = None 21 | 22 | if CTRL_BOARD: 23 | vid = 0x483 # Example VID for demonstration 24 | pid = 0x57AF # Example PID for demonstration 25 | 26 | com_port = list_vcp_with_vid_pid(vid, pid) 27 | if com_port is None: 28 | print("No device found") 29 | else: 30 | print("Device found at port: ", com_port) 31 | # Select communication port 32 | s = UART(com_port, timeout=5) 33 | else: 34 | s = UART(PORT_NAME, timeout=5) 35 | 36 | # Initialize the USTx controller object 37 | ustx_ctrl = CTRL_IF(s) 38 | 39 | print("Test PING") 40 | r = await ustx_ctrl.ping() 41 | format_and_print_hex(r) 42 | 43 | print("Get Temperature") 44 | for _ in range(10): # Loop 10 times 45 | r = await ustx_ctrl.get_temperature() 46 | packet = UartPacket(buffer=r) 47 | if packet.data_len == 4: 48 | try: 49 | temperature = struct.unpack(' 0: 31 | print(f"Number of TX7332 devices found: {num_tx_devices}") 32 | else: 33 | raise Exception("No TX7332 devices found.") 34 | 35 | print("Set TX7332 TI Config Waveform") 36 | for idx in range(num_tx_devices): 37 | interface.txdevice.apply_ti_config_file(txchip_id=idx, file_path="notebooks/ti_example.cfg") 38 | 39 | print("Get Trigger") 40 | trigger_setting = interface.txdevice.get_trigger_json() 41 | if trigger_setting: 42 | print(f"Trigger Setting: {trigger_setting}") 43 | else: 44 | print("Failed to get trigger setting.") 45 | 46 | print("Set Trigger") 47 | json_trigger_data = { 48 | "TriggerFrequencyHz": 25, 49 | "TriggerPulseCount": 0, 50 | "TriggerPulseWidthUsec": 20000, 51 | "TriggerPulseTrainInterval": 0, 52 | "TriggerPulseTrainCount": 0, 53 | "TriggerMode": 1, 54 | "ProfileIndex": 0, 55 | "ProfileIncrement": 0 56 | } 57 | trigger_setting = interface.txdevice.set_trigger_json(data=json_trigger_data) 58 | if trigger_setting: 59 | print(f"Trigger Setting: {trigger_setting}") 60 | else: 61 | print("Failed to set trigger setting.") 62 | 63 | print("Press enter to START trigger:") 64 | input() # Wait for the user to press Enter 65 | print("Starting Trigger...") 66 | if interface.txdevice.start_trigger(): 67 | print("Trigger Running Press enter to STOP:") 68 | input() # Wait for the user to press Enter 69 | if interface.txdevice.stop_trigger(): 70 | print("Trigger stopped successfully.") 71 | else: 72 | print("Failed to stop trigger.") 73 | else: 74 | print("Failed to get trigger setting.") 75 | -------------------------------------------------------------------------------- /notebooks/test_toggle_12v.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | 5 | from openlifu.io.LIFUInterface import LIFUInterface 6 | 7 | # set PYTHONPATH=%cd%\src;%PYTHONPATH% 8 | # python notebooks/test_toggle_12v.py 9 | 10 | print("Starting LIFU Test Script...") 11 | interface = LIFUInterface(TX_test_mode=False) 12 | 13 | tx_connected, hv_connected = interface.is_device_connected() 14 | if tx_connected and hv_connected: 15 | print("LIFU Device Fully connected.") 16 | else: 17 | print(f'LIFU Device NOT Fully Connected. TX: {tx_connected}, HV: {hv_connected}') 18 | 19 | if not hv_connected: 20 | print("HV Controller not connected.") 21 | sys.exit() 22 | 23 | print("Ping the device") 24 | interface.hvcontroller.ping() 25 | 26 | interface.hvcontroller.turn_12v_on() 27 | 28 | print("12v ON. Press enter to TURN OFF:") 29 | input() # Wait for user input 30 | 31 | interface.hvcontroller.turn_12v_off() 32 | -------------------------------------------------------------------------------- /notebooks/test_transmitter.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from openlifu.io.LIFUInterface import LIFUInterface 4 | 5 | # set PYTHONPATH=%cd%\src;%PYTHONPATH% 6 | # python notebooks/test_updated_if.py 7 | """ 8 | Test script to automate: 9 | 1. Connect to the device. 10 | 2. Test HVController: Turn HV on/off and check voltage. 11 | 3. Test Device functionality. 12 | """ 13 | print("Starting LIFU Test Script...") 14 | interface = LIFUInterface() 15 | tx_connected, hv_connected = interface.is_device_connected() 16 | if tx_connected and hv_connected: 17 | print("LIFU Device Fully connected.") 18 | else: 19 | print(f'LIFU Device NOT Fully Connected. TX: {tx_connected}, HV: {hv_connected}') 20 | 21 | print("Ping the device") 22 | interface.txdevice.ping() 23 | 24 | print("Get Temperature") 25 | temperature = interface.txdevice.get_temperature() 26 | print(f"Temperature: {temperature} °C") 27 | 28 | print("Enumerate TX7332 chips") 29 | num_tx_devices = interface.txdevice.enum_tx7332_devices() 30 | if num_tx_devices > 0: 31 | print(f"Number of TX7332 devices found: {num_tx_devices}") 32 | else: 33 | raise Exception("No TX7332 devices found.") 34 | 35 | print("Set TX7332 Demo Waveform") 36 | if interface.txdevice.demo_tx7332(): 37 | print("TX7332 demo waveform set successfully.") 38 | else: 39 | print("Failed to set TX7332 demo waveform.") 40 | 41 | print("Get Trigger") 42 | trigger_setting = interface.txdevice.get_trigger_json() 43 | if trigger_setting: 44 | print(f"Trigger Setting: {trigger_setting}") 45 | else: 46 | print("Failed to get trigger setting.") 47 | 48 | print("Set Trigger") 49 | json_trigger_data = { 50 | "TriggerFrequencyHz": 10, 51 | "TriggerMode": 1, 52 | "TriggerPulseCount": 0, 53 | "TriggerPulseWidthUsec": 20000 54 | } 55 | trigger_setting = interface.txdevice.set_trigger_json(data=json_trigger_data) 56 | if trigger_setting: 57 | print(f"Trigger Setting: {trigger_setting}") 58 | else: 59 | print("Failed to set trigger setting.") 60 | 61 | print("Press enter to START trigger:") 62 | input() # Wait for the user to press Enter 63 | print("Starting Trigger...") 64 | if interface.txdevice.start_trigger(): 65 | print("Trigger Running Press enter to STOP:") 66 | input() # Wait for the user to press Enter 67 | if interface.txdevice.stop_trigger(): 68 | print("Trigger stopped successfully.") 69 | else: 70 | print("Failed to stop trigger.") 71 | else: 72 | print("Failed to get trigger setting.") 73 | -------------------------------------------------------------------------------- /notebooks/test_transmitter2.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | 5 | import numpy as np 6 | 7 | from openlifu.bf.pulse import Pulse 8 | from openlifu.bf.sequence import Sequence 9 | from openlifu.geo import Point 10 | from openlifu.io.LIFUInterface import LIFUInterface 11 | from openlifu.plan.solution import Solution 12 | 13 | # set PYTHONPATH=%cd%\src;%PYTHONPATH% 14 | # python notebooks/test_transmitter2.py 15 | """ 16 | Test script to automate: 17 | 1. Connect to the device. 18 | 2. Test HVController: Turn HV on/off and check voltage. 19 | 3. Test Device functionality. 20 | """ 21 | print("Starting LIFU Test Script...") 22 | 23 | interface = LIFUInterface() 24 | tx_connected, hv_connected = interface.is_device_connected() 25 | 26 | if not tx_connected: 27 | print("TX device not connected. Attempting to turn on 12V...") 28 | sys.exit(1) 29 | 30 | print("Ping Transmitter device") 31 | interface.txdevice.ping() 32 | 33 | print("Get Version") 34 | version = interface.txdevice.get_version() 35 | print(f"Version: {version}") 36 | print("Get Temperature") 37 | temperature = interface.txdevice.get_temperature() 38 | print(f"Temperature: {temperature} °C") 39 | 40 | print("Enumerate TX7332 chips") 41 | num_tx_devices = interface.txdevice.enum_tx7332_devices() 42 | if num_tx_devices > 0: 43 | print(f"Number of TX7332 devices found: {num_tx_devices}") 44 | else: 45 | raise Exception("No TX7332 devices found.") 46 | 47 | # set focus 48 | xInput = 0 49 | yInput = 0 50 | zInput = 50 51 | 52 | frequency = 400e3 53 | voltage = 12.0 54 | duration = 2e-5 55 | 56 | pulse = Pulse(frequency=frequency, duration=duration) 57 | pt = Point(position=(xInput,yInput,zInput), units="mm") 58 | sequence = Sequence( 59 | pulse_interval=0.1, 60 | pulse_count=10, 61 | pulse_train_interval=1, 62 | pulse_train_count=1 63 | ) 64 | 65 | # Calculate delays and apodizations to perform beam forming 66 | 67 | solution = Solution( 68 | delays = np.zeros((1,64)), 69 | apodizations = np.ones((1,64)), 70 | pulse = pulse, 71 | voltage=voltage, 72 | sequence = sequence 73 | ) 74 | 75 | sol_dict = solution.to_dict() 76 | profile_index = 1 77 | profile_increment = True 78 | print("Set Solution") 79 | interface.txdevice.set_solution( 80 | pulse = sol_dict['pulse'], 81 | delays = sol_dict['delays'], 82 | apodizations= sol_dict['apodizations'], 83 | sequence= sol_dict['sequence'], 84 | mode = "continuous", 85 | profile_index=profile_index, 86 | profile_increment=profile_increment 87 | ) 88 | 89 | print("Get Trigger") 90 | trigger_setting = interface.txdevice.get_trigger_json() 91 | if trigger_setting: 92 | print(f"Trigger Setting: {trigger_setting}") 93 | else: 94 | print("Failed to get trigger setting.") 95 | 96 | print("Starting Trigger...") 97 | if interface.txdevice.start_trigger(): 98 | print("Trigger Running Press enter to STOP:") 99 | input() # Wait for the user to press Enter 100 | if interface.txdevice.stop_trigger(): 101 | print("Trigger stopped successfully.") 102 | else: 103 | print("Failed to stop trigger.") 104 | else: 105 | print("Failed to get trigger setting.") 106 | -------------------------------------------------------------------------------- /notebooks/test_tx_dfu.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | import time 5 | 6 | from openlifu.io.LIFUInterface import LIFUInterface 7 | 8 | # set PYTHONPATH=%cd%\src;%PYTHONPATH% 9 | # python notebooks/test_tx_dfu.py 10 | 11 | print("Starting LIFU Test Script...") 12 | interface = LIFUInterface() 13 | tx_connected, hv_connected = interface.is_device_connected() 14 | 15 | if not hv_connected: 16 | print("✅ LIFU Console not connected.") 17 | sys.exit(1) 18 | 19 | if not tx_connected: 20 | print("TX device not connected. Attempting to turn on 12V...") 21 | interface.hvcontroller.turn_12v_on() 22 | 23 | # Give time for the TX device to power up and enumerate over USB 24 | time.sleep(2) 25 | 26 | # Cleanup and recreate interface to reinitialize USB devices 27 | interface.stop_monitoring() 28 | del interface 29 | time.sleep(5) # Short delay before recreating 30 | 31 | print("Reinitializing LIFU interface after powering 12V...") 32 | interface = LIFUInterface() 33 | 34 | # Re-check connection 35 | tx_connected, hv_connected = interface.is_device_connected() 36 | 37 | if tx_connected and hv_connected: 38 | print("✅ LIFU Device fully connected.") 39 | else: 40 | print("❌ LIFU Device NOT fully connected.") 41 | print(f" TX Connected: {tx_connected}") 42 | print(f" HV Connected: {hv_connected}") 43 | sys.exit(1) 44 | 45 | 46 | print("Ping the device") 47 | if not interface.txdevice.ping(): 48 | print("❌ failed to communicate with transmit module") 49 | sys.exit(1) 50 | 51 | print("Get Version") 52 | version = interface.txdevice.get_version() 53 | print(f"Version: {version}") 54 | 55 | 56 | # Ask the user for confirmation 57 | user_input = input("Do you want to Enter DFU Mode? (y/n): ").strip().lower() 58 | 59 | if user_input == 'y': 60 | print("Enter DFU mode") 61 | if interface.txdevice.enter_dfu(): 62 | print("Successful.") 63 | elif user_input == 'n': 64 | pass 65 | else: 66 | print("Invalid input. Please enter 'y' or 'n'.") 67 | -------------------------------------------------------------------------------- /notebooks/test_tx_trigger.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | import time 5 | 6 | from openlifu.io.LIFUInterface import LIFUInterface 7 | 8 | # set PYTHONPATH=%cd%\src;%PYTHONPATH% 9 | # python notebooks/test_tx_trigger.py 10 | 11 | def get_user_input(): 12 | while True: 13 | print("\nEnter parameters (or 'x' to exit):") 14 | try: 15 | freq = input("Trigger Frequency (Hz): ") 16 | if freq.lower() == 'x': 17 | return None 18 | 19 | pulse_width = input("Pulse Width (μs): ") 20 | if pulse_width.lower() == 'x': 21 | return None 22 | 23 | return { 24 | "freq": float(freq), 25 | "pulse_width": float(pulse_width) 26 | } 27 | except ValueError: 28 | print("Invalid input. Please enter numbers or 'x' to exit.") 29 | 30 | def main(): 31 | print("Starting LIFU Test Script...") 32 | interface = LIFUInterface() 33 | tx_connected, hv_connected = interface.is_device_connected() 34 | 35 | if not tx_connected and not hv_connected: 36 | print("✅ LIFU Console not connected.") 37 | sys.exit(1) 38 | 39 | if not tx_connected: 40 | print("TX device not connected. Attempting to turn on 12V...") 41 | interface.hvcontroller.turn_12v_on() 42 | time.sleep(2) 43 | 44 | interface.stop_monitoring() 45 | del interface 46 | time.sleep(3) 47 | 48 | print("Reinitializing LIFU interface after powering 12V...") 49 | interface = LIFUInterface() 50 | tx_connected, hv_connected = interface.is_device_connected() 51 | 52 | if tx_connected and hv_connected: 53 | print("✅ LIFU Device fully connected.") 54 | else: 55 | print("❌ LIFU Device NOT fully connected.") 56 | print(f" TX Connected: {tx_connected}") 57 | print(f" HV Connected: {hv_connected}") 58 | sys.exit(1) 59 | 60 | print("Ping the device") 61 | if not interface.txdevice.ping(): 62 | print("❌ Failed comms with txdevice.") 63 | sys.exit(1) 64 | 65 | while True: 66 | params = get_user_input() 67 | if params is None: 68 | print("Exiting...") 69 | break 70 | 71 | json_trigger_data = { 72 | "TriggerFrequencyHz": params["freq"], 73 | "TriggerPulseCount": 1, 74 | "TriggerPulseWidthUsec": params["pulse_width"], 75 | "TriggerPulseTrainInterval": 0, 76 | "TriggerPulseTrainCount": 0, 77 | "TriggerMode": 1, 78 | "ProfileIndex": 0, 79 | "ProfileIncrement": 0 80 | } 81 | 82 | trigger_setting = interface.txdevice.set_trigger_json(data=json_trigger_data) 83 | 84 | if trigger_setting: 85 | print(f"Trigger Setting: {trigger_setting}") 86 | else: 87 | print("Failed to set trigger setting.") 88 | continue 89 | 90 | if interface.txdevice.start_trigger(): 91 | print("Trigger Running. Press Enter to STOP:") 92 | input() # Wait for the user to press Enter 93 | if interface.txdevice.stop_trigger(): 94 | print("Trigger stopped successfully.") 95 | else: 96 | print("Failed to stop trigger.") 97 | 98 | if __name__ == "__main__": 99 | main() 100 | -------------------------------------------------------------------------------- /notebooks/test_updated_pwr.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from openlifu.io.LIFUInterface import LIFUInterface 4 | 5 | # set PYTHONPATH=%cd%\src;%PYTHONPATH% 6 | # python notebooks/test_updated_pwr.py 7 | """ 8 | Test script to automate: 9 | 1. Connect to the device. 10 | 2. Test HVController: Turn HV on/off and check voltage. 11 | 3. Test Device functionality. 12 | """ 13 | print("Starting LIFU Test Script...") 14 | interface = LIFUInterface() 15 | tx_connected, hv_connected = interface.is_device_connected() 16 | if tx_connected and hv_connected: 17 | print("LIFU Device Fully connected.") 18 | else: 19 | print(f'LIFU Device NOT Fully Connected. TX: {tx_connected}, HV: {hv_connected}') 20 | 21 | 22 | print("Ping the device") 23 | interface.hvcontroller.ping() 24 | 25 | print("Toggle LED") 26 | interface.hvcontroller.toggle_led() 27 | 28 | print("Get Version") 29 | version = interface.hvcontroller.get_version() 30 | print(f"Version: {version}") 31 | 32 | print("Echo Data") 33 | echo, echo_len = interface.hvcontroller.echo(echo_data=b'Hello LIFU!') 34 | if echo_len > 0: 35 | print(f"Echo: {echo.decode('utf-8')}") # Echo: Hello LIFU! 36 | else: 37 | print("Echo failed.") 38 | 39 | print("Get HW ID") 40 | hw_id = interface.hvcontroller.get_hardware_id() 41 | print(f"HWID: {hw_id}") 42 | 43 | print("Test 12V...") 44 | if interface.hvcontroller.turn_12v_on(): 45 | print("12V ON Press enter to TURN OFF:") 46 | input() # Wait for the user to press Enter 47 | if interface.hvcontroller.turn_12v_off(): 48 | print("12V OFF.") 49 | else: 50 | print("Failed to turn off 12V") 51 | else: 52 | print("Failed to turn on 12V.") 53 | 54 | # Set High Voltage Level 55 | print("Set HV Power to +/- 24V") 56 | if interface.hvcontroller.set_voltage(voltage=24.0): 57 | print("Voltage set to 24.0 V.") 58 | else: 59 | print("Failed to set voltage.") 60 | 61 | # Get Set High Voltage Setting 62 | print("Get HV Setting") 63 | read_set_voltage = interface.hvcontroller.get_voltage() 64 | print(f"Voltage set to {read_set_voltage} V.") 65 | 66 | 67 | print("Test HV Supply...") 68 | if interface.hvcontroller.turn_hv_on(): 69 | print("HV ON Press enter to TURN OFF:") 70 | input() # Wait for the user to press Enter 71 | if interface.hvcontroller.turn_hv_off(): 72 | print("HV OFF.") 73 | else: 74 | print("Failed to turn off HV") 75 | else: 76 | print("Failed to turn on HV.") 77 | 78 | print("Reset DevConsoleice:") 79 | # Ask the user for confirmation 80 | user_input = input("Do you want to reset the Console? (y/n): ").strip().lower() 81 | 82 | if user_input == 'y': 83 | if interface.hvcontroller.soft_reset(): 84 | print("Reset Successful.") 85 | elif user_input == 'n': 86 | print("Reset canceled.") 87 | else: 88 | print("Invalid input. Please enter 'y' or 'n'.") 89 | -------------------------------------------------------------------------------- /noxfile.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import argparse 4 | import shutil 5 | from pathlib import Path 6 | 7 | import nox 8 | 9 | DIR = Path(__file__).parent.resolve() 10 | 11 | nox.needs_version = ">=2024.3.2" 12 | nox.options.sessions = ["lint", "pylint", "tests"] 13 | nox.options.default_venv_backend = "uv|virtualenv" 14 | 15 | 16 | @nox.session 17 | def lint(session: nox.Session) -> None: 18 | """ 19 | Run the linter. 20 | """ 21 | session.install("pre-commit") 22 | session.run( 23 | "pre-commit", "run", "--all-files", "--show-diff-on-failure", *session.posargs 24 | ) 25 | 26 | 27 | @nox.session() 28 | def pylint(session: nox.Session) -> None: 29 | """ 30 | Run PyLint. 31 | """ 32 | # This needs to be installed into the package environment, and is slower 33 | # than a pre-commit check 34 | session.install(".", "pylint") 35 | session.run("pylint", "openlifu", *session.posargs) 36 | 37 | 38 | @nox.session() 39 | def tests(session: nox.Session) -> None: 40 | """ 41 | Run the unit and regular tests. 42 | """ 43 | session.install(".[test]") 44 | session.run("pytest", *session.posargs) 45 | 46 | 47 | @nox.session(reuse_venv=True) 48 | def docs(session: nox.Session) -> None: 49 | """ 50 | Build the docs. Pass "--serve" to serve. Pass "-b linkcheck" to check links. 51 | """ 52 | 53 | parser = argparse.ArgumentParser() 54 | parser.add_argument("--serve", action="store_true", help="Serve after building") 55 | parser.add_argument( 56 | "-b", dest="builder", default="html", help="Build target (default: html)" 57 | ) 58 | args, posargs = parser.parse_known_args(session.posargs) 59 | 60 | if args.builder != "html" and args.serve: 61 | session.error("Must not specify non-HTML builder with --serve") 62 | 63 | extra_installs = ["sphinx-autobuild"] if args.serve else [] 64 | 65 | session.install("-e.[docs]", *extra_installs) 66 | session.chdir("docs") 67 | 68 | if args.builder == "linkcheck": 69 | session.run( 70 | "sphinx-build", "-b", "linkcheck", ".", "_build/linkcheck", *posargs 71 | ) 72 | return 73 | 74 | shared_args = ( 75 | "-n", # nitpicky mode 76 | "-T", # full tracebacks 77 | f"-b={args.builder}", 78 | ".", 79 | f"_build/{args.builder}", 80 | *posargs, 81 | ) 82 | 83 | if args.serve: 84 | session.run("sphinx-autobuild", *shared_args) 85 | else: 86 | session.run("sphinx-build", "--keep-going", *shared_args) 87 | 88 | 89 | @nox.session 90 | def build_api_docs(session: nox.Session) -> None: 91 | """ 92 | Build (regenerate) API docs. 93 | """ 94 | 95 | session.install("sphinx") 96 | session.chdir("docs") 97 | session.run( 98 | "sphinx-apidoc", 99 | "-o", 100 | "api/", 101 | "--module-first", 102 | "--no-toc", 103 | "--force", 104 | "../src/openlifu", 105 | ) 106 | 107 | 108 | @nox.session 109 | def build(session: nox.Session) -> None: 110 | """ 111 | Build an SDist and wheel. 112 | """ 113 | 114 | build_path = DIR.joinpath("build") 115 | if build_path.exists(): 116 | shutil.rmtree(build_path) 117 | 118 | session.install("build") 119 | session.run("python", "-m", "build") 120 | -------------------------------------------------------------------------------- /scripts/standardize_database.py: -------------------------------------------------------------------------------- 1 | """This is a utility script for developers to read in and write back out the dvc database. 2 | It is useful for standardizing the format of the example dvc data, and also for checking that the database 3 | mostly still works. 4 | 5 | To use this script, install openlifu to a python environment and then run the script providing the database folder as an argument: 6 | 7 | ``` 8 | python standardize_database.py db_dvc/ 9 | ``` 10 | 11 | A couple of known issues to watch out for: 12 | - The date_modified of Sessions gets updated, as it should, when this is run. But we don't care about that change. 13 | - The netCDF simulation output files (.nc files) are modified for some reason each time they are written out. It's probably a similar 14 | thing going on with some kind of timestamp being embedded in the file. 15 | """ 16 | from __future__ import annotations 17 | 18 | import pathlib 19 | import shutil 20 | import sys 21 | import tempfile 22 | 23 | from openlifu.db import Database 24 | from openlifu.db.database import OnConflictOpts 25 | 26 | if len(sys.argv) != 2: 27 | raise RuntimeError("Provide exactly one argument: the path to the database folder.") 28 | db = Database(sys.argv[1]) 29 | 30 | db.write_protocol_ids(db.get_protocol_ids()) 31 | for protocol_id in db.get_protocol_ids(): 32 | protocol = db.load_protocol(protocol_id) 33 | assert protocol_id == protocol.id 34 | db.write_protocol(protocol, on_conflict=OnConflictOpts.OVERWRITE) 35 | 36 | db.write_transducer_ids(db.get_transducer_ids()) 37 | for transducer_id in db.get_transducer_ids(): 38 | transducer = db.load_transducer(transducer_id) 39 | assert transducer_id == transducer.id 40 | db.write_transducer(transducer, on_conflict=OnConflictOpts.OVERWRITE) 41 | 42 | db.write_subject_ids(db.get_subject_ids()) 43 | for subject_id in db.get_subject_ids(): 44 | subject = db.load_subject(subject_id) 45 | assert subject_id == subject.id 46 | db.write_subject(subject, on_conflict=OnConflictOpts.OVERWRITE) 47 | 48 | db.write_volume_ids(subject_id, db.get_volume_ids(subject_id)) 49 | for volume_id in db.get_volume_ids(subject_id): 50 | volume_info = db.get_volume_info(subject_id, volume_id) 51 | assert volume_info["id"] == volume_id 52 | volume_data_abspath = pathlib.Path(volume_info["data_abspath"]) 53 | 54 | # The weird file move here is because of a quirk in Database: 55 | # - you can't just edit the volume metadata, you have to write the metadata json and volume data file together 56 | # - if you try to provide the volume_data_abspath as the data path you get a SameFileError from shutil which 57 | # refuses to do the copy. These things can be fixed but it's a niche use case so I'd rather work around it in this script. 58 | with tempfile.TemporaryDirectory() as tmpdir: 59 | tmpdir = pathlib.Path(tmpdir) 60 | moved_vol_abspath = tmpdir / volume_data_abspath.name 61 | shutil.move(volume_data_abspath, moved_vol_abspath) 62 | db.write_volume(subject_id, volume_id, volume_info["name"], moved_vol_abspath, on_conflict=OnConflictOpts.OVERWRITE) 63 | 64 | session_ids = db.get_session_ids(subject.id) 65 | db.write_session_ids(subject_id, session_ids) 66 | for session_id in session_ids: 67 | session = db.load_session(subject, session_id) 68 | assert session.id == session_id 69 | assert session.subject_id == subject.id 70 | db.write_session(subject, session, on_conflict=OnConflictOpts.OVERWRITE) 71 | 72 | solution_ids = db.get_solution_ids(session.subject_id, session.id) 73 | db.write_solution_ids(session, solution_ids) 74 | for solution_id in solution_ids: 75 | solution = db.load_solution(session, solution_id) 76 | assert solution.id == solution_id 77 | assert solution.simulation_result['p_min'].shape[0] == solution.num_foci() 78 | db.write_solution(session, solution, on_conflict=OnConflictOpts.OVERWRITE) 79 | 80 | run_ids = db.get_run_ids(subject_id, session_id) 81 | db.write_run_ids(subject_id, session_id, run_ids) 82 | # (Runs are read only at the moment so it's just the runs.json and no individual runs to standardize) 83 | 84 | db.write_user_ids(db.get_user_ids()) 85 | for user_id in db.get_user_ids(): 86 | user = db.load_user(user_id) 87 | assert user_id == user.id 88 | db.write_user(user, on_conflict=OnConflictOpts.OVERWRITE) 89 | -------------------------------------------------------------------------------- /src/openlifu/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (c) 2023 Openwater. All rights reserved. 3 | 4 | openlifu: Openwater Focused Ultrasound Toolkit 5 | """ 6 | 7 | from __future__ import annotations 8 | 9 | from openlifu.bf import ( 10 | ApodizationMethod, 11 | DelayMethod, 12 | FocalPattern, 13 | Pulse, 14 | Sequence, 15 | apod_methods, 16 | delay_methods, 17 | focal_patterns, 18 | ) 19 | from openlifu.db import Database, User 20 | 21 | #from . import bf, db, io, plan, seg, sim, xdc, geo 22 | from openlifu.geo import Point 23 | from openlifu.io.LIFUInterface import LIFUInterface 24 | from openlifu.plan import Protocol, Solution 25 | from openlifu.seg import ( 26 | AIR, 27 | MATERIALS, 28 | SKULL, 29 | STANDOFF, 30 | TISSUE, 31 | WATER, 32 | Material, 33 | SegmentationMethod, 34 | seg_methods, 35 | ) 36 | from openlifu.sim import SimSetup 37 | from openlifu.virtual_fit import VirtualFitOptions, run_virtual_fit 38 | from openlifu.xdc import Transducer 39 | 40 | from ._version import version as __version__ 41 | 42 | __all__ = [ 43 | "Point", 44 | "Transducer", 45 | "Protocol", 46 | "Solution", 47 | "Material", 48 | "SegmentationMethod", 49 | "seg_methods", 50 | "MATERIALS", 51 | "WATER", 52 | "TISSUE", 53 | "SKULL", 54 | "AIR", 55 | "STANDOFF", 56 | "DelayMethod", 57 | "ApodizationMethod", 58 | "Pulse", 59 | "Sequence", 60 | "FocalPattern", 61 | "focal_patterns", 62 | "delay_methods", 63 | "apod_methods", 64 | "SimSetup", 65 | "Database", 66 | "User", 67 | "VirtualFitOptions", 68 | "run_virtual_fit", 69 | "LIFUInterface", 70 | "__version__", 71 | ] 72 | -------------------------------------------------------------------------------- /src/openlifu/_version.pyi: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | version: str 4 | version_tuple: tuple[int, int, int] | tuple[int, int, int, str, str] 5 | -------------------------------------------------------------------------------- /src/openlifu/bf/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .apod_methods import ApodizationMethod 4 | from .delay_methods import DelayMethod 5 | from .focal_patterns import FocalPattern, SinglePoint, Wheel 6 | from .pulse import Pulse 7 | from .sequence import Sequence 8 | 9 | __all__ = [ 10 | "DelayMethod", 11 | "ApodizationMethod", 12 | "Wheel", 13 | "FocalPattern", 14 | "SinglePoint", 15 | "Pulse", 16 | "Sequence" 17 | ] 18 | -------------------------------------------------------------------------------- /src/openlifu/bf/apod_methods/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .apodmethod import ApodizationMethod 4 | from .maxangle import MaxAngle 5 | from .piecewiselinear import PiecewiseLinear 6 | from .uniform import Uniform 7 | 8 | __all__ = [ 9 | "ApodizationMethod", 10 | "Uniform", 11 | "MaxAngle", 12 | "PiecewiseLinear", 13 | ] 14 | -------------------------------------------------------------------------------- /src/openlifu/bf/apod_methods/apodmethod.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC, abstractmethod 4 | from dataclasses import dataclass 5 | from typing import Any 6 | 7 | import numpy as np 8 | import xarray as xa 9 | 10 | from openlifu.bf import apod_methods 11 | from openlifu.geo import Point 12 | from openlifu.xdc import Transducer 13 | 14 | 15 | @dataclass 16 | class ApodizationMethod(ABC): 17 | @abstractmethod 18 | def calc_apodization(self, arr: Transducer, target: Point, params: xa.Dataset, transform:np.ndarray | None=None) -> Any: 19 | pass 20 | 21 | def to_dict(self): 22 | d = self.__dict__.copy() 23 | d['class'] = self.__class__.__name__ 24 | return d 25 | 26 | @staticmethod 27 | def from_dict(d): 28 | d = d.copy() 29 | short_classname = d.pop("class") 30 | module_dict = apod_methods.__dict__ 31 | class_constructor = module_dict[short_classname] 32 | return class_constructor(**d) 33 | -------------------------------------------------------------------------------- /src/openlifu/bf/apod_methods/maxangle.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import Annotated 5 | 6 | import numpy as np 7 | import xarray as xa 8 | 9 | from openlifu.bf.apod_methods import ApodizationMethod 10 | from openlifu.geo import Point 11 | from openlifu.util.annotations import OpenLIFUFieldData 12 | from openlifu.util.units import getunittype 13 | from openlifu.xdc import Transducer 14 | 15 | 16 | @dataclass 17 | class MaxAngle(ApodizationMethod): 18 | max_angle: Annotated[float, OpenLIFUFieldData("Maximum acceptance angle", "Maximum acceptance angle for each element from the vector normal to the element surface")] = 30.0 19 | """Maximum acceptance angle for each element from the vector normal to the element surface""" 20 | 21 | units: Annotated[str, OpenLIFUFieldData("Angle units", "Angle units")] = "deg" 22 | """Angle units""" 23 | 24 | def __post_init__(self): 25 | if not isinstance(self.max_angle, (int, float)): 26 | raise TypeError(f"Max angle must be a number, got {type(self.max_angle).__name__}.") 27 | if self.max_angle < 0: 28 | raise ValueError(f"Max angle must be non-negative, got {self.max_angle}.") 29 | if getunittype(self.units) != "angle": 30 | raise ValueError(f"Units must be an angle type, got {self.units}.") 31 | 32 | def calc_apodization(self, arr: Transducer, target: Point, params: xa.Dataset, transform:np.ndarray | None=None): 33 | target_pos = target.get_position(units="m") 34 | matrix = transform if transform is not None else np.eye(4) 35 | angles = np.array([el.angle_to_point(target_pos, units="m", matrix=matrix, return_as=self.units) for el in arr.elements]) 36 | apod = np.zeros(arr.numelements()) 37 | apod[angles <= self.max_angle] = 1 38 | return apod 39 | -------------------------------------------------------------------------------- /src/openlifu/bf/apod_methods/piecewiselinear.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import Annotated 5 | 6 | import numpy as np 7 | import xarray as xa 8 | 9 | from openlifu.bf.apod_methods import ApodizationMethod 10 | from openlifu.geo import Point 11 | from openlifu.util.annotations import OpenLIFUFieldData 12 | from openlifu.util.units import getunittype 13 | from openlifu.xdc import Transducer 14 | 15 | 16 | @dataclass 17 | class PiecewiseLinear(ApodizationMethod): 18 | zero_angle: Annotated[float, OpenLIFUFieldData("Zero Apodization Angle", "Angle at and beyond which the piecewise linear apodization is 0%")] = 90.0 19 | """Angle at and beyond which the piecewise linear apodization is 0%""" 20 | 21 | rolloff_angle: Annotated[float, OpenLIFUFieldData("Rolloff start angle", "Angle below which the piecewise linear apodization is 100%")] = 45.0 22 | """Angle below which the piecewise linear apodization is 100%""" 23 | 24 | units: Annotated[str, OpenLIFUFieldData("Angle units", "Angle units")] = "deg" 25 | """Angle units""" 26 | 27 | def __post_init__(self): 28 | if not isinstance(self.zero_angle, (int, float)): 29 | raise TypeError(f"Zero angle must be a number, got {type(self.zero_angle).__name__}.") 30 | if self.zero_angle < 0: 31 | raise ValueError(f"Zero angle must be non-negative, got {self.zero_angle}.") 32 | if not isinstance(self.rolloff_angle, (int, float)): 33 | raise TypeError(f"Rolloff angle must be a number, got {type(self.rolloff_angle).__name__}.") 34 | if self.rolloff_angle < 0: 35 | raise ValueError(f"Rolloff angle must be non-negative, got {self.rolloff_angle}.") 36 | if self.rolloff_angle >= self.zero_angle: 37 | raise ValueError(f"Rolloff angle must be less than zero angle, got {self.rolloff_angle} >= {self.zero_angle}.") 38 | if getunittype(self.units) != "angle": 39 | raise ValueError(f"Units must be an angle type, got {self.units}.") 40 | 41 | def calc_apodization(self, arr: Transducer, target: Point, params: xa.Dataset, transform:np.ndarray | None=None): 42 | target_pos = target.get_position(units="m") 43 | matrix = transform if transform is not None else np.eye(4) 44 | angles = np.array([el.angle_to_point(target_pos, units="m", matrix=matrix, return_as=self.units) for el in arr.elements]) 45 | apod = np.zeros(arr.numelements()) 46 | f = ((self.zero_angle - angles) / (self.zero_angle - self.rolloff_angle)) 47 | apod = np.maximum(0, np.minimum(1, f)) 48 | return apod 49 | -------------------------------------------------------------------------------- /src/openlifu/bf/apod_methods/uniform.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import Annotated 5 | 6 | import numpy as np 7 | import xarray as xa 8 | 9 | from openlifu.bf.apod_methods import ApodizationMethod 10 | from openlifu.geo import Point 11 | from openlifu.util.annotations import OpenLIFUFieldData 12 | from openlifu.xdc import Transducer 13 | 14 | 15 | @dataclass 16 | class Uniform(ApodizationMethod): 17 | value: Annotated[float, OpenLIFUFieldData("Value", "Uniform apodization value between 0 and 1.")] = 1.0 18 | """Uniform apodization value between 0 and 1.""" 19 | 20 | def calc_apodization(self, arr: Transducer, target: Point, params: xa.Dataset, transform:np.ndarray | None=None): 21 | return np.full(arr.numelements(), self.value) 22 | -------------------------------------------------------------------------------- /src/openlifu/bf/delay_methods/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .delaymethod import DelayMethod 4 | from .direct import Direct 5 | 6 | __all__ = [ 7 | "DelayMethod", 8 | "Direct", 9 | ] 10 | -------------------------------------------------------------------------------- /src/openlifu/bf/delay_methods/delaymethod.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC, abstractmethod 4 | from dataclasses import dataclass 5 | 6 | import numpy as np 7 | import xarray as xa 8 | 9 | from openlifu.bf import delay_methods 10 | from openlifu.geo import Point 11 | from openlifu.xdc import Transducer 12 | 13 | 14 | @dataclass 15 | class DelayMethod(ABC): 16 | @abstractmethod 17 | def calc_delays(self, arr: Transducer, target: Point, params: xa.Dataset, transform:np.ndarray | None=None): 18 | pass 19 | 20 | def to_dict(self): 21 | d = self.__dict__.copy() 22 | d['class'] = self.__class__.__name__ 23 | return d 24 | 25 | @staticmethod 26 | def from_dict(d): 27 | d = d.copy() 28 | short_classname = d.pop("class") 29 | module_dict = delay_methods.__dict__ 30 | class_constructor = module_dict[short_classname] 31 | return class_constructor(**d) 32 | -------------------------------------------------------------------------------- /src/openlifu/bf/delay_methods/direct.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import Annotated 5 | 6 | import numpy as np 7 | import xarray as xa 8 | 9 | from openlifu.bf.delay_methods import DelayMethod 10 | from openlifu.geo import Point 11 | from openlifu.util.annotations import OpenLIFUFieldData 12 | from openlifu.xdc import Transducer 13 | 14 | 15 | @dataclass 16 | class Direct(DelayMethod): 17 | c0: Annotated[float, OpenLIFUFieldData("Speed of Sound (m/s)", "Speed of sound in the medium (m/s)")] = 1480.0 18 | """Speed of sound in the medium (m/s)""" 19 | 20 | def __post_init__(self): 21 | if not isinstance(self.c0, (int, float)): 22 | raise TypeError("Speed of sound must be a number") 23 | if self.c0 <= 0: 24 | raise ValueError("Speed of sound must be greater than 0") 25 | self.c0 = float(self.c0) 26 | 27 | def calc_delays(self, arr: Transducer, target: Point, params: xa.Dataset | None=None, transform:np.ndarray | None=None): 28 | if params is None: 29 | c = self.c0 30 | else: 31 | c = params['sound_speed'].attrs['ref_value'] 32 | target_pos = target.get_position(units="m") 33 | matrix = transform if transform is not None else np.eye(4) 34 | dists = np.array([el.distance_to_point(target_pos, units="m", matrix=matrix) for el in arr.elements]) 35 | tof = dists / c 36 | delays = max(tof) - tof 37 | return delays 38 | -------------------------------------------------------------------------------- /src/openlifu/bf/focal_patterns/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .focal_pattern import FocalPattern 4 | from .single import SinglePoint 5 | from .wheel import Wheel 6 | 7 | __all__ = [ 8 | "FocalPattern", 9 | "SinglePoint", 10 | "Wheel", 11 | ] 12 | -------------------------------------------------------------------------------- /src/openlifu/bf/focal_patterns/focal_pattern.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC, abstractmethod 4 | from dataclasses import dataclass 5 | from typing import Annotated 6 | 7 | from openlifu.bf import focal_patterns 8 | from openlifu.geo import Point 9 | from openlifu.util.annotations import OpenLIFUFieldData 10 | from openlifu.util.units import getunittype 11 | 12 | 13 | @dataclass 14 | class FocalPattern(ABC): 15 | """ 16 | Abstract base class for representing a focal pattern 17 | """ 18 | 19 | target_pressure: Annotated[float, OpenLIFUFieldData("Target pressure", "Target pressure of the focal pattern in given units")] = 1.0 20 | """Target pressure of the focal pattern in given units""" 21 | 22 | units: Annotated[str, OpenLIFUFieldData("Pressure units", "Pressure units")] = "Pa" 23 | """Pressure units""" 24 | 25 | def __post_init__(self): 26 | if self.target_pressure <= 0: 27 | raise ValueError("Target pressure must be greater than 0") 28 | if not isinstance(self.units, str): 29 | raise TypeError("Units must be a string") 30 | if getunittype(self.units) != 'pressure': 31 | raise ValueError(f"Units must be a pressure unit, got {self.units}") 32 | 33 | @abstractmethod 34 | def get_targets(self, target: Point): 35 | """ 36 | Get the targets of the focal pattern 37 | 38 | :param target: Target point of the focal pattern 39 | :returns: List of target points 40 | """ 41 | pass 42 | 43 | @abstractmethod 44 | def num_foci(self): 45 | """ 46 | Get the number of foci in the focal pattern 47 | 48 | :returns: Number of foci 49 | """ 50 | pass 51 | 52 | def to_dict(self): 53 | """ 54 | Convert the focal pattern to a dictionary 55 | 56 | :returns: Dictionary of the focal pattern parameters 57 | """ 58 | d = self.__dict__.copy() 59 | d['class'] = self.__class__.__name__ 60 | return d 61 | 62 | @staticmethod 63 | def from_dict(d): 64 | """ 65 | Create a focal pattern from a dictionary 66 | 67 | :param d: Dictionary of the focal pattern parameters 68 | :returns: FocalPattern object 69 | """ 70 | d = d.copy() 71 | short_classname = d.pop("class") 72 | module_dict = focal_patterns.__dict__ 73 | class_constructor = module_dict[short_classname] 74 | return class_constructor(**d) 75 | -------------------------------------------------------------------------------- /src/openlifu/bf/focal_patterns/single.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | 5 | from openlifu.bf.focal_patterns import FocalPattern 6 | from openlifu.geo import Point 7 | 8 | 9 | @dataclass 10 | class SinglePoint(FocalPattern): 11 | """ 12 | Class for representing a single focus 13 | 14 | :ivar target_pressure: Target pressure of the focal pattern in Pa 15 | """ 16 | def get_targets(self, target: Point): 17 | """ 18 | Get the targets of the focal pattern 19 | 20 | :param target: Target point of the focal pattern 21 | :returns: List of target points 22 | """ 23 | return [target.copy()] 24 | 25 | def num_foci(self): 26 | """ 27 | Get the number of foci in the focal pattern 28 | 29 | :returns: Number of foci (1) 30 | """ 31 | return 1 32 | -------------------------------------------------------------------------------- /src/openlifu/bf/focal_patterns/wheel.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import Annotated 5 | 6 | import numpy as np 7 | 8 | from openlifu.bf.focal_patterns import FocalPattern 9 | from openlifu.geo import Point 10 | from openlifu.util.annotations import OpenLIFUFieldData 11 | 12 | 13 | @dataclass 14 | class Wheel(FocalPattern): 15 | """ 16 | Class for representing a wheel pattern 17 | """ 18 | 19 | center: Annotated[bool, OpenLIFUFieldData("Include center point?", "Whether to include the center for the wheel pattern")] = True 20 | """Whether to include the center for the wheel pattern""" 21 | 22 | num_spokes: Annotated[int, OpenLIFUFieldData("Number of spokes", "Number of spokes in the wheel pattern")] = 4 23 | """Number of spokes in the wheel pattern""" 24 | 25 | spoke_radius: Annotated[float, OpenLIFUFieldData("Spoke radius", "Radius of the spokes in the wheel pattern")] = 1.0 # mm 26 | """Radius of the spokes in the wheel pattern""" 27 | 28 | distance_units: Annotated[str, OpenLIFUFieldData("Units", "Units of the wheel pattern parameters")] = "mm" 29 | """Units of the wheel pattern parameters""" 30 | 31 | def __post_init__(self): 32 | if not isinstance(self.center, bool): 33 | raise TypeError(f"Center must be a boolean, got {type(self.center).__name__}.") 34 | if not isinstance(self.num_spokes, int) or self.num_spokes < 1: 35 | raise ValueError(f"Number of spokes must be a positive integer, got {self.num_spokes}.") 36 | if not isinstance(self.spoke_radius, (int, float)) or self.spoke_radius <= 0: 37 | raise ValueError(f"Spoke radius must be a positive number, got {self.spoke_radius}.") 38 | super().__post_init__() 39 | 40 | def get_targets(self, target: Point): 41 | """ 42 | Get the targets of the focal pattern 43 | 44 | :param target: Target point of the focal pattern 45 | :returns: List of target points 46 | """ 47 | if self.center: 48 | targets = [target.copy()] 49 | targets[0].id = f"{target.id}_center" 50 | targets[0].id = f"{target.id} (Center)" 51 | else: 52 | targets = [] 53 | m = target.get_matrix(center_on_point=True) 54 | for i in range(self.num_spokes): 55 | theta = 2*np.pi*i/self.num_spokes 56 | local_position = self.spoke_radius * np.array([np.cos(theta), np.sin(theta), 0.0]) 57 | position = np.dot(m, np.append(local_position, 1.0))[:3] 58 | spoke = Point(id=f"{target.id}_{np.rad2deg(theta):.0f}deg", 59 | name=f"{target.name} ({np.rad2deg(theta):.0f}°)", 60 | position=position, 61 | units=self.distance_units, 62 | radius=target.radius) 63 | targets.append(spoke) 64 | return targets 65 | 66 | def num_foci(self) -> int: 67 | """ 68 | Get the number of foci in the focal pattern 69 | 70 | :returns: Number of foci 71 | """ 72 | return int(self.center) + self.num_spokes 73 | -------------------------------------------------------------------------------- /src/openlifu/bf/pulse.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import Annotated 5 | 6 | import numpy as np 7 | import pandas as pd 8 | 9 | from openlifu.util.annotations import OpenLIFUFieldData 10 | from openlifu.util.dict_conversion import DictMixin 11 | 12 | 13 | @dataclass 14 | class Pulse(DictMixin): 15 | """ 16 | Class for representing a sinusoidal pulse 17 | """ 18 | 19 | frequency: Annotated[float, OpenLIFUFieldData("Frequency (Hz)", "Frequency of the pulse in Hz")] = 1.0 # Hz 20 | """Frequency of the pulse in Hz""" 21 | 22 | amplitude: Annotated[float, OpenLIFUFieldData("Amplitude (AU)", "Amplitude of the pulse (between 0 and 1). ")] = 1.0 # AU 23 | """Amplitude of the pulse in arbitrary units (AU) between 0 and 1""" 24 | 25 | duration: Annotated[float, OpenLIFUFieldData("Duration (s)", "Duration of the pulse in s")] = 1.0 # s 26 | """Duration of the pulse in s""" 27 | 28 | def __post_init__(self): 29 | if self.frequency <= 0: 30 | raise ValueError("Frequency must be greater than 0") 31 | if self.amplitude < 0 or self.amplitude > 1: 32 | raise ValueError("Amplitude must be between 0 and 1") 33 | if self.duration <= 0: 34 | raise ValueError("Duration must be greater than 0") 35 | 36 | def calc_pulse(self, t: np.array): 37 | """ 38 | Calculate the pulse at the given times 39 | 40 | :param t: Array of times to calculate the pulse at (s) 41 | :returns: Array of pulse values at the given times 42 | """ 43 | return self.amplitude * np.sin(2*np.pi*self.frequency*t) 44 | 45 | def calc_time(self, dt: float): 46 | """ 47 | Calculate the time array for the pulse for a particular timestep 48 | 49 | :param dt: Time step (s) 50 | :returns: Array of times for the pulse (s) 51 | """ 52 | return np.arange(0, self.duration, dt) 53 | 54 | def get_table(self): 55 | """ 56 | Get a table of the pulse parameters 57 | 58 | :returns: Pandas DataFrame of the pulse parameters 59 | """ 60 | records = [{"Name": "Frequency", "Value": self.frequency, "Unit": "Hz"}, 61 | {"Name": "Amplitude", "Value": self.amplitude, "Unit": "AU"}, 62 | {"Name": "Duration", "Value": self.duration, "Unit": "s"}] 63 | return pd.DataFrame.from_records(records) 64 | -------------------------------------------------------------------------------- /src/openlifu/bf/sequence.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import Annotated 5 | 6 | import pandas as pd 7 | 8 | from openlifu.util.annotations import OpenLIFUFieldData 9 | from openlifu.util.dict_conversion import DictMixin 10 | 11 | 12 | @dataclass 13 | class Sequence(DictMixin): 14 | """ 15 | Class for representing a sequence of pulses 16 | """ 17 | 18 | pulse_interval: Annotated[float, OpenLIFUFieldData("Pulse interval (s)", "Interval between pulses in the sequence (s)")] = 1.0 # s 19 | """Interval between pulses in the sequence (s)""" 20 | 21 | pulse_count: Annotated[int, OpenLIFUFieldData("Pulse count", "Number of pulses in the sequence")] = 1 22 | """Number of pulses in the sequence""" 23 | 24 | pulse_train_interval: Annotated[float, OpenLIFUFieldData("Pulse train interval (s)", "Interval between pulse trains in the sequence (s)")] = 1.0 # s 25 | """Interval between pulse trains in the sequence (s)""" 26 | 27 | pulse_train_count: Annotated[int, OpenLIFUFieldData("Pulse train count", "Number of pulse trains in the sequence")] = 1 28 | """Number of pulse trains in the sequence""" 29 | 30 | def __post_init__(self): 31 | if self.pulse_interval <= 0: 32 | raise ValueError("Pulse interval must be positive") 33 | if self.pulse_count <= 0: 34 | raise ValueError("Pulse count must be positive") 35 | if self.pulse_train_interval < 0: 36 | raise ValueError("Pulse train interval must be non-negative") 37 | elif (self.pulse_train_interval > 0) and (self.pulse_train_interval < (self.pulse_interval * self.pulse_count)): 38 | raise ValueError("Pulse train interval must be greater than or equal to the total pulse interval") 39 | if self.pulse_train_count <= 0: 40 | raise ValueError("Pulse train count must be positive") 41 | 42 | def get_table(self) -> pd.DataFrame: 43 | """ 44 | Get a table of the sequence parameters 45 | 46 | :returns: Pandas DataFrame of the sequence parameters 47 | """ 48 | records = [ 49 | {"Name": "Pulse Interval", "Value": self.pulse_interval, "Unit": "s"}, 50 | {"Name": "Pulse Count", "Value": self.pulse_count, "Unit": ""}, 51 | {"Name": "Pulse Train Interval", "Value": self.pulse_train_interval, "Unit": "s"}, 52 | {"Name": "Pulse Train Count", "Value": self.pulse_train_count, "Unit": ""} 53 | ] 54 | return pd.DataFrame.from_records(records) 55 | 56 | def get_pulse_train_duration(self) -> float: 57 | """ 58 | Get the duration of a single pulse train in seconds 59 | 60 | :returns: Duration of a single pulse train in seconds 61 | """ 62 | return self.pulse_interval * self.pulse_count 63 | 64 | def get_sequence_duration(self) -> float: 65 | """ 66 | Get the total duration of the sequence in seconds 67 | 68 | :returns: Total duration of the sequence in seconds 69 | """ 70 | if self.pulse_train_interval == 0: 71 | interval = self.get_pulsetrain_duration() 72 | else: 73 | interval = self.pulse_train_interval 74 | return interval * self.pulse_train_count 75 | -------------------------------------------------------------------------------- /src/openlifu/db/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .database import Database 4 | from .session import Session 5 | from .subject import Subject 6 | from .user import User 7 | 8 | __all__ = [ 9 | "Subject", 10 | "Session", 11 | "Database", 12 | "User", 13 | ] 14 | -------------------------------------------------------------------------------- /src/openlifu/db/subject.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | from dataclasses import dataclass, field 5 | from pathlib import Path 6 | from typing import Annotated 7 | 8 | from openlifu.util.annotations import OpenLIFUFieldData 9 | from openlifu.util.dict_conversion import DictMixin 10 | from openlifu.util.strings import sanitize 11 | 12 | 13 | @dataclass 14 | class Subject(DictMixin): 15 | """ 16 | Class representing a subject 17 | """ 18 | 19 | id: Annotated[str | None, OpenLIFUFieldData("Subject ID", "ID of the subject")] = None 20 | """ID of the subject""" 21 | 22 | name: Annotated[str | None, OpenLIFUFieldData("Subject name", "Name of the subject")] = None 23 | """Name of the subject""" 24 | 25 | attrs: Annotated[dict, OpenLIFUFieldData("Attributes", "Dictionary of attributes")] = field(default_factory=dict) 26 | """Dictionary of attributes""" 27 | 28 | def __post_init__(self): 29 | if self.id is None and self.name is None: 30 | self.id = "subject" 31 | if self.id is None: 32 | self.id = sanitize(self.name, "snake") 33 | if self.name is None: 34 | self.name = self.id 35 | 36 | @staticmethod 37 | def from_file(filename): 38 | """ 39 | Create a subject from a file 40 | 41 | :param filename: Name of the file to read 42 | :returns: Subject object 43 | """ 44 | with open(filename) as f: 45 | return Subject.from_dict(json.load(f)) 46 | 47 | def to_file(self, filename): 48 | """ 49 | Write the subject to a file 50 | 51 | :param filename: Name of the file to write 52 | """ 53 | Path(filename).parent.mkdir(exist_ok=True) 54 | with open(filename, 'w') as f: 55 | json.dump(self.to_dict(), f, indent=4) 56 | -------------------------------------------------------------------------------- /src/openlifu/db/user.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | import logging 5 | from dataclasses import dataclass, field 6 | from pathlib import Path 7 | from typing import Annotated, Any, Dict, List 8 | 9 | from openlifu.util.annotations import OpenLIFUFieldData 10 | 11 | 12 | @dataclass 13 | class User: 14 | id: Annotated[str, OpenLIFUFieldData("User ID", "The unique identifier of the user")] = "user" 15 | """The unique identifier of the user""" 16 | 17 | password_hash: Annotated[str, OpenLIFUFieldData("Password hash", "A hashed user password for authentication.")] = "" 18 | """A hashed user password for authentication.""" 19 | 20 | roles: Annotated[List[str], OpenLIFUFieldData("Roles", "A list of roles")] = field(default_factory=list) 21 | """A list of roles""" 22 | 23 | name: Annotated[str, OpenLIFUFieldData("User name", "The name of the user")] = "User" 24 | """The name of the user""" 25 | 26 | description: Annotated[str, OpenLIFUFieldData("Description", "A description of the user")] = "" 27 | """A description of the user""" 28 | 29 | def __post_init__(self): 30 | self.logger = logging.getLogger(__name__) 31 | 32 | @staticmethod 33 | def from_dict(d : Dict[str,Any]) -> User: 34 | return User(**d) 35 | 36 | def to_dict(self): 37 | return { 38 | "id": self.id, 39 | "password_hash": self.password_hash, 40 | "roles": self.roles, 41 | "name": self.name, 42 | "description": self.description, 43 | } 44 | 45 | @staticmethod 46 | def from_file(filename): 47 | with open(filename) as f: 48 | d = json.load(f) 49 | return User.from_dict(d) 50 | 51 | @staticmethod 52 | def from_json(json_string : str) -> User: 53 | """Load a User from a json string""" 54 | return User.from_dict(json.loads(json_string)) 55 | 56 | def to_json(self, compact:bool) -> str: 57 | """Serialize a User to a json string 58 | 59 | Args: 60 | compact: if enabled then the string is compact (not pretty). Disable for pretty. 61 | 62 | Returns: A json string representing the complete User object. 63 | """ 64 | if compact: 65 | return json.dumps(self.to_dict(), separators=(',', ':')) 66 | else: 67 | return json.dumps(self.to_dict(), indent=4) 68 | 69 | def to_file(self, filename: str): 70 | """ 71 | Save the user to a file 72 | 73 | Args: 74 | filename: Name of the file 75 | """ 76 | Path(filename).parent.mkdir(exist_ok=True, parents=True) 77 | with open(filename, 'w') as file: 78 | file.write(self.to_json(compact=False)) 79 | -------------------------------------------------------------------------------- /src/openlifu/io/LIFUConfig.py: -------------------------------------------------------------------------------- 1 | 2 | # Packet Types 3 | from __future__ import annotations 4 | 5 | OW_ACK = 0xE0 6 | OW_NAK = 0xE1 7 | OW_CMD = 0xE2 8 | OW_RESP = 0xE3 9 | OW_DATA = 0xE4 10 | OW_ONE_WIRE = 0xE5 11 | OW_TX7332 = 0xE6 12 | OW_AFE_READ = 0xE7 13 | OW_AFE_SEND = 0xE8 14 | OW_I2C_PASSTHRU = 0xE9 15 | OW_CONTROLLER = 0xEA 16 | OW_POWER = 0xEB 17 | OW_ONEWIRE_RESP = 0xEC 18 | OW_ERROR = 0xEF 19 | 20 | OW_SUCCESS = 0x00 21 | OW_UNKNOWN_COMMAND = 0xFC 22 | OW_BAD_CRC = 0xFD 23 | OW_INVALID_PACKET = 0xFE 24 | OW_UNKNOWN_ERROR = 0xFF 25 | 26 | # Global Commands 27 | OW_CMD_PING = 0x00 28 | OW_CMD_PONG = 0x01 29 | OW_CMD_VERSION = 0x02 30 | OW_CMD_ECHO = 0x03 31 | OW_CMD_TOGGLE_LED = 0x04 32 | OW_CMD_HWID = 0x05 33 | OW_CMD_GET_TEMP = 0x06 34 | OW_CMD_GET_AMBIENT = 0x07 35 | OW_CMD_DFU = 0x0D 36 | OW_CMD_NOP = 0x0E 37 | OW_CMD_RESET = 0x0F 38 | 39 | # Controller Commands 40 | OW_CTRL_SET_SWTRIG = 0x13 41 | OW_CTRL_GET_SWTRIG = 0x14 42 | OW_CTRL_START_SWTRIG = 0x15 43 | OW_CTRL_STOP_SWTRIG = 0x16 44 | OW_CTRL_STATUS_SWTRIG = 0x17 45 | OW_CTRL_RESET = 0x1F 46 | 47 | # TX7332 Commands 48 | OW_TX7332_STATUS = 0x20 49 | OW_TX7332_ENUM = 0x21 50 | OW_TX7332_WREG = 0x22 51 | OW_TX7332_RREG = 0x23 52 | OW_TX7332_WBLOCK = 0x24 53 | OW_TX7332_VWREG = 0x25 54 | OW_TX7332_VWBLOCK = 0x26 55 | OW_TX7332_DEMO = 0x2D 56 | OW_TX7332_RESET = 0x2F 57 | 58 | # Power Commands 59 | OW_POWER_STATUS = 0x30 60 | OW_POWER_SET_HV = 0x31 61 | OW_POWER_GET_HV = 0x32 62 | OW_POWER_HV_ON = 0x33 63 | OW_POWER_HV_OFF = 0x34 64 | OW_POWER_12V_ON = 0x35 65 | OW_POWER_12V_OFF = 0x36 66 | OW_POWER_GET_TEMP1 = 0x37 67 | OW_POWER_GET_TEMP2 = 0x38 68 | OW_POWER_SET_FAN = 0x39 69 | OW_POWER_GET_FAN = 0x3A 70 | OW_POWER_SET_RGB = 0x3B 71 | OW_POWER_GET_RGB = 0x3C 72 | OW_POWER_GET_HVON = 0x3D 73 | OW_POWER_GET_12VON = 0x3E 74 | OW_POWER_SET_DACS = 0x3F 75 | -------------------------------------------------------------------------------- /src/openlifu/io/LIFUSignal.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | class LIFUSignal: 5 | def __init__(self): 6 | # Initialize a list to store connected slots (callback functions) 7 | self._slots = [] 8 | 9 | def connect(self, slot): 10 | """ 11 | Connect a slot (callback function) to the signal. 12 | 13 | Args: 14 | slot (callable): A callable to be invoked when the signal is emitted. 15 | """ 16 | if callable(slot) and slot not in self._slots: 17 | self._slots.append(slot) 18 | 19 | def disconnect(self, slot): 20 | """ 21 | Disconnect a slot (callback function) from the signal. 22 | 23 | Args: 24 | slot (callable): The callable to disconnect. 25 | """ 26 | if slot in self._slots: 27 | self._slots.remove(slot) 28 | 29 | def emit(self, *args, **kwargs): 30 | """ 31 | Emit the signal, invoking all connected slots. 32 | 33 | Args: 34 | *args: Positional arguments to pass to the connected slots. 35 | **kwargs: Keyword arguments to pass to the connected slots. 36 | """ 37 | for slot in self._slots: 38 | slot(*args, **kwargs) 39 | -------------------------------------------------------------------------------- /src/openlifu/io/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from openlifu.io.LIFUInterface import LIFUInterface, LIFUInterfaceStatus 4 | 5 | __all__ = [ 6 | "LIFUInterface", 7 | "LIFUInterfaceStatus", 8 | ] 9 | -------------------------------------------------------------------------------- /src/openlifu/nav/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenwaterHealth/OpenLIFU-python/c83a1337118e78051dc79b6c68eaef3d5f781ac9/src/openlifu/nav/__init__.py -------------------------------------------------------------------------------- /src/openlifu/nav/meshroom_pipelines/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenwaterHealth/OpenLIFU-python/c83a1337118e78051dc79b6c68eaef3d5f781ac9/src/openlifu/nav/meshroom_pipelines/__init__.py -------------------------------------------------------------------------------- /src/openlifu/nav/meshroom_pipelines/draft_pipeline.mg: -------------------------------------------------------------------------------- 1 | { 2 | "header": { 3 | "nodesVersions": { 4 | "FeatureExtraction": "1.3", 5 | "StructureFromMotion": "3.3", 6 | "ImageMatching": "2.0", 7 | "PrepareDenseScene": "3.1", 8 | "MeshFiltering": "3.0", 9 | "FeatureMatching": "2.0", 10 | "Texturing": "6.0", 11 | "CameraInit": "9.0", 12 | "Meshing": "7.0", 13 | "Publish": "1.3" 14 | }, 15 | "releaseVersion": "2023.3.0", 16 | "fileVersion": "1.1", 17 | "template": true 18 | }, 19 | "graph": { 20 | "Texturing_1": { 21 | "nodeType": "Texturing", 22 | "position": [ 23 | 1600, 24 | 0 25 | ], 26 | "inputs": { 27 | "input": "{Meshing_1.output}", 28 | "imagesFolder": "{PrepareDenseScene_1.output}", 29 | "inputMesh": "{MeshFiltering_1.outputMesh}", 30 | "colorMapping": { 31 | "enable": true, 32 | "colorMappingFileType": "png" 33 | } 34 | } 35 | }, 36 | "Meshing_1": { 37 | "nodeType": "Meshing", 38 | "position": [ 39 | 1200, 40 | 0 41 | ], 42 | "inputs": { 43 | "input": "{PrepareDenseScene_1.input}" 44 | } 45 | }, 46 | "FeatureExtraction_1": { 47 | "nodeType": "FeatureExtraction", 48 | "position": [ 49 | 200, 50 | 0 51 | ], 52 | "inputs": { 53 | "input": "{CameraInit_1.output}", 54 | "forceCpuExtraction": false 55 | } 56 | }, 57 | "StructureFromMotion_1": { 58 | "nodeType": "StructureFromMotion", 59 | "position": [ 60 | 800, 61 | 0 62 | ], 63 | "inputs": { 64 | "input": "{FeatureMatching_1.input}", 65 | "featuresFolders": "{FeatureMatching_1.featuresFolders}", 66 | "matchesFolders": [ 67 | "{FeatureMatching_1.output}" 68 | ], 69 | "describerTypes": "{FeatureMatching_1.describerTypes}" 70 | } 71 | }, 72 | "CameraInit_1": { 73 | "nodeType": "CameraInit", 74 | "position": [ 75 | 0, 76 | 0 77 | ], 78 | "inputs": {} 79 | }, 80 | "MeshFiltering_1": { 81 | "nodeType": "MeshFiltering", 82 | "position": [ 83 | 1400, 84 | 0 85 | ], 86 | "inputs": { 87 | "inputMesh": "{Meshing_1.outputMesh}" 88 | } 89 | }, 90 | "FeatureMatching_1": { 91 | "nodeType": "FeatureMatching", 92 | "position": [ 93 | 600, 94 | 0 95 | ], 96 | "inputs": { 97 | "input": "{FeatureExtraction_1.input}", 98 | "featuresFolders": "{FeatureExtraction_1.output}", 99 | "imagePairsList": "{}", 100 | "describerTypes": "{FeatureExtraction_1.describerTypes}" 101 | } 102 | }, 103 | "PrepareDenseScene_1": { 104 | "nodeType": "PrepareDenseScene", 105 | "position": [ 106 | 1000, 107 | 0 108 | ], 109 | "inputs": { 110 | "input": "{StructureFromMotion_1.output}" 111 | } 112 | }, 113 | "Publish_1": { 114 | "nodeType": "Publish", 115 | "position": [ 116 | 2272, 117 | 290 118 | ], 119 | "inputs": { 120 | "inputFiles": [ 121 | "{Texturing_1.output}", 122 | "{Texturing_1.outputMesh}", 123 | "{Texturing_1.outputMaterial}", 124 | "{Texturing_1.outputTextures}" 125 | ] 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/openlifu/nav/modnet_checkpoints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenwaterHealth/OpenLIFU-python/c83a1337118e78051dc79b6c68eaef3d5f781ac9/src/openlifu/nav/modnet_checkpoints/__init__.py -------------------------------------------------------------------------------- /src/openlifu/plan/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .param_constraint import ParameterConstraint 4 | from .protocol import Protocol 5 | from .run import Run 6 | from .solution import Solution 7 | from .solution_analysis import SolutionAnalysis, SolutionAnalysisOptions 8 | from .target_constraints import TargetConstraints 9 | 10 | __all__ = [ 11 | "Protocol", 12 | "Solution", 13 | "Run", 14 | "SolutionAnalysis", 15 | "SolutionAnalysisOptions", 16 | "TargetConstraints", 17 | "ParameterConstraint" 18 | ] 19 | -------------------------------------------------------------------------------- /src/openlifu/plan/param_constraint.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import Annotated, Literal, Tuple 5 | 6 | from openlifu.util.annotations import OpenLIFUFieldData 7 | from openlifu.util.dict_conversion import DictMixin 8 | 9 | PARAM_STATUS_SYMBOLS = { 10 | "ok": "✅", 11 | "warning": "❗", 12 | "error": "❌" 13 | } 14 | 15 | @dataclass 16 | class ParameterConstraint(DictMixin): 17 | operator: Annotated[Literal['<', '<=', '>', '>=', 'within', 'inside', 'outside', 'outside_inclusive'], OpenLIFUFieldData("Constraint operator", "Constraint operator used to evaluate parameter values")] 18 | """Constraint operator used to evaluate parameter values""" 19 | 20 | warning_value: Annotated[float | int | Tuple[float | int, float | int] | None, OpenLIFUFieldData("Warning value", "Threshold or range that triggers a warning")] = None 21 | """Threshold or range that triggers a warning""" 22 | 23 | error_value: Annotated[float | int | Tuple[float | int, float | int] | None, OpenLIFUFieldData("Error value", "Threshold or range that triggers an error")] = None 24 | """Threshold or range that triggers an error""" 25 | 26 | def __post_init__(self): 27 | if self.warning_value is None and self.error_value is None: 28 | raise ValueError("At least one of warning_value or error_value must be set") 29 | if self.operator in ['within', 'inside', 'outside', 'outside_inclusive']: 30 | if self.warning_value and (not isinstance(self.warning_value, tuple) or len(self.warning_value) != 2 or self.warning_value[0] >= self.warning_value[1]): 31 | raise ValueError("Warning value must be a sorted tuple of two numbers") 32 | if self.error_value and (not isinstance(self.error_value, tuple) or len(self.error_value) != 2 or self.error_value[0] >= self.error_value[1]): 33 | raise ValueError("Error value must be a sorted tuple of two numbers") 34 | elif self.operator in ['<', '<=', '>', '>=']: 35 | if self.warning_value is not None and not isinstance(self.warning_value, (int, float)): 36 | raise ValueError("Warning value must be a single value") 37 | if self.error_value is not None and not isinstance(self.error_value, (int, float)): 38 | raise ValueError("Error value must be a single value") 39 | 40 | @staticmethod 41 | def compare(value, operator, threshold) -> bool: 42 | if operator == '<': 43 | return value < threshold 44 | elif operator == '<=': 45 | return value <= threshold 46 | elif operator == '>': 47 | return value > threshold 48 | elif operator == '>=': 49 | return value >= threshold 50 | elif operator == 'within': 51 | return threshold[0] < value < threshold[1] 52 | elif operator == 'inside': 53 | return threshold[0] <= value <= threshold[1] 54 | elif operator == 'outside': 55 | return value < threshold[0] or value > threshold[1] 56 | elif operator == 'outside_inclusive': 57 | return value <= threshold[0] or value >= threshold[1] 58 | else: 59 | raise ValueError(f"Unsupported operator: {operator}") 60 | 61 | def is_warning(self, value: float | int) -> bool: 62 | if self.warning_value is not None: 63 | return not self.compare(value, self.operator, self.warning_value) 64 | return False 65 | 66 | def is_error(self, value: float | int) -> bool: 67 | if self.error_value is not None: 68 | return not self.compare(value, self.operator, self.error_value) 69 | return False 70 | 71 | def get_status(self, value: float) -> str: 72 | if self.is_error(value): 73 | return "error" 74 | elif self.is_warning(value): 75 | return "warning" 76 | else: 77 | return "ok" 78 | 79 | def get_status_symbol(self, value: float) -> str: 80 | return PARAM_STATUS_SYMBOLS[self.get_status(value)] 81 | -------------------------------------------------------------------------------- /src/openlifu/plan/run.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | from dataclasses import dataclass 5 | from pathlib import Path 6 | from typing import Annotated, Any, Dict 7 | 8 | from openlifu.util.annotations import OpenLIFUFieldData 9 | from openlifu.util.json import PYFUSEncoder 10 | 11 | 12 | @dataclass 13 | class Run: 14 | """ 15 | Class representing a run 16 | """ 17 | 18 | id: Annotated[str | None, OpenLIFUFieldData("Run ID", "ID of the run")] = None 19 | """ID of the run""" 20 | 21 | name: Annotated[str | None, OpenLIFUFieldData("Run name", "Name of the run")] = None 22 | """Name of the run""" 23 | 24 | success_flag: Annotated[bool | None, OpenLIFUFieldData("Success?", "True when run was successful, False otherwise")] = None 25 | """True when run was successful, False otherwise""" 26 | 27 | note: Annotated[str | None, OpenLIFUFieldData("Run notes", "Large text containing notes about the run")] = None 28 | """Large text containing notes about the run""" 29 | 30 | session_id: Annotated[str | None, OpenLIFUFieldData("Session ID", "Session ID")] = None 31 | """Session ID""" 32 | 33 | solution_id: Annotated[str | None, OpenLIFUFieldData("Solution ID", "Solution ID")] = None 34 | """Solution ID""" 35 | 36 | @staticmethod 37 | def from_file(filename): 38 | """ 39 | Create a Run from a file 40 | 41 | :param filename: Name of the file to read 42 | :returns: Run object 43 | """ 44 | with open(filename) as f: 45 | d = json.load(f) 46 | return Run.from_dict(d) 47 | 48 | @staticmethod 49 | def from_json(json_string : str) -> Run: 50 | """Load a Run from a json string""" 51 | return Run.from_dict(json.loads(json_string)) 52 | 53 | @staticmethod 54 | def from_dict(d : Dict[str, Any]) -> Run: 55 | return Run(**d) 56 | 57 | def to_dict(self): 58 | """ 59 | Convert the run to a dictionary 60 | 61 | :returns: Dictionary of run parameters 62 | """ 63 | d = self.__dict__.copy() 64 | return d 65 | 66 | def to_json(self, compact:bool) -> str: 67 | """Serialize a Run to a json string 68 | 69 | Args: 70 | compact: if enabled then the string is compact (not pretty). Disable for pretty. 71 | 72 | Returns: A json string representing the complete Run object. 73 | """ 74 | if compact: 75 | return json.dumps(self.to_dict(), separators=(',', ':'), cls=PYFUSEncoder) 76 | else: 77 | return json.dumps(self.to_dict(), indent=4, cls=PYFUSEncoder) 78 | 79 | def to_file(self, filename): 80 | """ 81 | Save the Run to a file 82 | 83 | :param filename: Name of the file 84 | """ 85 | Path(filename).parent.parent.mkdir(exist_ok=True) 86 | Path(filename).parent.mkdir(exist_ok=True) 87 | with open(filename, 'w') as file: 88 | file.write(self.to_json(compact=False)) 89 | -------------------------------------------------------------------------------- /src/openlifu/plan/target_constraints.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from dataclasses import dataclass 5 | from typing import Annotated 6 | 7 | from openlifu.util.annotations import OpenLIFUFieldData 8 | from openlifu.util.dict_conversion import DictMixin 9 | from openlifu.util.units import getunittype 10 | 11 | 12 | @dataclass 13 | class TargetConstraints(DictMixin): 14 | """A class for storing target constraints. 15 | 16 | Target constraints are used to define the acceptable range of 17 | positions for a target. For example, a target constraint could 18 | be used to define the acceptable range of values for the x position 19 | of a target. 20 | """ 21 | 22 | dim: Annotated[str, OpenLIFUFieldData("Constrained dimension ID", "The dimension ID being constrained")] = "x" 23 | """The dimension ID being constrained""" 24 | 25 | name: Annotated[str, OpenLIFUFieldData("Constrained dimension name", "The name of the dimension being constrained")] = "dim" 26 | """The name of the dimension being constrained""" 27 | 28 | units: Annotated[str, OpenLIFUFieldData("Dimension units", "The units of the dimension being constrained")] = "m" 29 | """The units of the dimension being constrained""" 30 | 31 | min: Annotated[float, OpenLIFUFieldData("Minimum allowed value", "The minimum value of the dimension")] = float("-inf") 32 | """The minimum value of the dimension""" 33 | 34 | max: Annotated[float, OpenLIFUFieldData("Maximum allowed value", "The maximum value of the dimension")] = float("inf") 35 | """The maximum value of the dimension""" 36 | 37 | def __post_init__(self): 38 | if not isinstance(self.dim, str): 39 | raise TypeError("Dimension ID must be a string") 40 | if not isinstance(self.name, str): 41 | raise TypeError("Dimension name must be a string") 42 | if not isinstance(self.units, str): 43 | raise TypeError("Dimension units must be a string") 44 | if getunittype(self.units) != 'distance': 45 | raise ValueError(f"Units must be a length unit, got {self.units}") 46 | if not isinstance(self.min, (int, float)): 47 | raise TypeError("Minimum value must be a number") 48 | if not isinstance(self.max, (int, float)): 49 | raise TypeError("Maximum value must be a number") 50 | if self.min > self.max: 51 | raise ValueError("Minimum value cannot be greater than maximum value") 52 | 53 | def check_bounds(self, pos: float): 54 | """Check if the given position is within bounds.""" 55 | 56 | if (pos < self.min) or (pos > self.max): 57 | logging.error(msg=f"The position {pos} at dimension {self.name} is not within bounds [{self.min}, {self.max}]!") 58 | raise ValueError(f"The position {pos} at dimension {self.name} is not within bounds [{self.min}, {self.max}]!") 59 | -------------------------------------------------------------------------------- /src/openlifu/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenwaterHealth/OpenLIFU-python/c83a1337118e78051dc79b6c68eaef3d5f781ac9/src/openlifu/py.typed -------------------------------------------------------------------------------- /src/openlifu/seg/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from . import seg_methods 4 | from .material import AIR, MATERIALS, SKULL, STANDOFF, TISSUE, WATER, Material 5 | from .seg_method import SegmentationMethod 6 | 7 | __all__ = [ 8 | "Material", 9 | "MATERIALS", 10 | "WATER", 11 | "TISSUE", 12 | "SKULL", 13 | "AIR", 14 | "STANDOFF", 15 | "SegmentationMethod", 16 | "seg_methods", 17 | ] 18 | -------------------------------------------------------------------------------- /src/openlifu/seg/seg_method.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import copy 4 | from abc import ABC, abstractmethod 5 | from dataclasses import dataclass, field 6 | from typing import Annotated, Any 7 | 8 | import numpy as np 9 | import xarray as xa 10 | 11 | from openlifu.seg.material import MATERIALS, PARAM_INFO, Material 12 | from openlifu.util.annotations import OpenLIFUFieldData 13 | 14 | 15 | @dataclass 16 | class SegmentationMethod(ABC): 17 | materials: Annotated[dict[str, Material], OpenLIFUFieldData("Segmentation materials", "Dictionary mapping of label names to material definitions used during segmentation")] = field(default_factory=lambda: MATERIALS.copy()) 18 | """Dictionary mapping of label names to material definitions used during segmentation""" 19 | 20 | ref_material: Annotated[str, OpenLIFUFieldData("Reference material", "Reference material ID to use")] = "water" 21 | """Reference material ID to use""" 22 | 23 | def __post_init__(self): 24 | if self.materials is None: 25 | self.materials = MATERIALS.copy() 26 | if not isinstance(self.materials, dict): 27 | raise TypeError(f"Materials must be a dictionary, got {type(self.materials).__name__}.") 28 | if not all(isinstance(m, Material) for m in self.materials.values()): 29 | raise TypeError("All materials must be instances of Material class.") 30 | if self.ref_material not in self.materials: 31 | raise ValueError(f"Reference material {self.ref_material} not found.") 32 | 33 | @abstractmethod 34 | def _segment(self, volume: xa.DataArray) -> xa.DataArray: 35 | pass 36 | 37 | def to_dict(self) -> dict[str, Any]: 38 | d = self.__dict__.copy() 39 | d['materials'] = { k: v.to_dict() for k, v in self.materials.items() } 40 | d['class'] = self.__class__.__name__ 41 | return d 42 | 43 | @staticmethod 44 | def from_dict(d: dict) -> SegmentationMethod: 45 | from openlifu.seg import seg_methods 46 | if not isinstance(d, dict): # previous implementations might pass str 47 | raise TypeError(f"Expected dict for from_dict, got {type(d).__name__}") 48 | 49 | d = copy.deepcopy(d) 50 | short_classname = d.pop("class") 51 | 52 | # Recursively construct Material instances 53 | materials_dict = d.get("materials") 54 | if materials_dict is not None: 55 | d["materials"] = { 56 | k: v if isinstance(v, Material) else Material.from_dict(v) 57 | for k, v in materials_dict.items() 58 | } 59 | 60 | # Ignore ref_material if class is `UniformWater` or `UniformTissue` 61 | if short_classname in ["UniformWater", "UniformTissue"]: 62 | d.pop("ref_material") 63 | class_constructor = getattr(seg_methods, short_classname) 64 | return class_constructor(**d) 65 | 66 | def _material_indices(self, materials: dict | None = None): 67 | materials = self.materials if materials is None else materials 68 | return {material_id: i for i, material_id in enumerate(materials.keys())} 69 | 70 | def _map_params(self, seg: xa.DataArray, materials: dict | None = None): 71 | materials = self.materials if materials is None else materials 72 | material_dict = self._material_indices(materials=materials) 73 | params = xa.Dataset() 74 | ref_mat = materials[self.ref_material] 75 | for param_id in PARAM_INFO: 76 | info = Material.param_info(param_id) 77 | param = xa.DataArray(np.zeros(seg.shape), coords=seg.coords, attrs={"units": info["units"], "long_name": info["name"], "ref_value": ref_mat.get_param(param_id)}) 78 | for material_id, material in materials.items(): 79 | midx = material_dict[material_id] 80 | param.data[seg.data == midx] = getattr(material, param_id) 81 | params[param_id] = param 82 | params.attrs['ref_material'] = ref_mat 83 | return params 84 | 85 | def seg_params(self, volume: xa.DataArray, materials: dict | None = None): 86 | materials = self.materials if materials is None else materials 87 | seg = self._segment(volume) 88 | params = self._map_params(seg, materials=materials) 89 | return params 90 | 91 | def ref_params(self, coords: xa.Coordinates): 92 | seg = self._ref_segment(coords) 93 | params = self._map_params(seg) 94 | return params 95 | 96 | def _ref_segment(self, coords: xa.Coordinates): 97 | material_dict = self._material_indices() 98 | m_idx = material_dict[self.ref_material] 99 | sz = list(coords.sizes.values()) 100 | seg = xa.DataArray(np.full(sz, m_idx, dtype=int), coords=coords) 101 | return seg 102 | -------------------------------------------------------------------------------- /src/openlifu/seg/seg_methods/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .uniform import UniformSegmentation, UniformTissue, UniformWater 4 | 5 | __all__ = [ 6 | "UniformSegmentation", 7 | "UniformWater", 8 | "UniformTissue", 9 | ] 10 | -------------------------------------------------------------------------------- /src/openlifu/seg/seg_methods/uniform.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import xarray as xa 4 | 5 | from openlifu.seg.material import MATERIALS, Material 6 | from openlifu.seg.seg_method import SegmentationMethod 7 | 8 | 9 | class UniformSegmentation(SegmentationMethod): 10 | def _segment(self, volume: xa.DataArray): 11 | return self._ref_segment(volume.coords) 12 | 13 | class UniformTissue(UniformSegmentation): 14 | """ Assigns the tissue material to all voxels in the volume. """ 15 | def __init__(self, materials: dict[str, Material] | None = None): 16 | if materials is None: 17 | materials = MATERIALS.copy() 18 | super().__init__(materials=materials, ref_material="tissue") 19 | 20 | class UniformWater(UniformSegmentation): 21 | """ Assigns the water material to all voxels in the volume. """ 22 | def __init__(self, materials: dict[str, Material] | None = None): 23 | if materials is None: 24 | materials = MATERIALS.copy() 25 | super().__init__(materials=materials, ref_material="water") 26 | -------------------------------------------------------------------------------- /src/openlifu/sim/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from . import kwave_if 4 | from .kwave_if import run_simulation 5 | from .sim_setup import SimSetup 6 | 7 | __all__ = [ 8 | "SimSetup", 9 | "run_simulation", 10 | "kwave_if", 11 | ] 12 | -------------------------------------------------------------------------------- /src/openlifu/util/annotations.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Annotated, NamedTuple 4 | 5 | 6 | class OpenLIFUFieldData(NamedTuple): 7 | """ 8 | A lightweight named tuple representing a name and annotation for the fields 9 | of a dataclass. For example, the Graph dataclass may have fields associated 10 | with this type: 11 | 12 | ```python 13 | class Graph: 14 | units: Annotated[str, OpenLIFUFieldData("Units", "The units of the graph")] = "mm" 15 | dim_names: Annotated[ 16 | Tuple[str, str, str], 17 | OpenLIFUFieldData("Dimensions", "The name of the dimensions of the graph."), 18 | ] = ("lat", "ele", "ax") 19 | ``` 20 | 21 | Annotated[] does not interfere with runtime behavior or type compatibility. 22 | """ 23 | 24 | name: Annotated[str | None, "The name of the dataclass field."] 25 | description: Annotated[str | None, "The description of the dataclass field."] 26 | -------------------------------------------------------------------------------- /src/openlifu/util/checkgpu.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pynvml import nvmlDeviceGetCount, nvmlInit, nvmlShutdown 4 | 5 | 6 | def gpu_available() -> bool: 7 | """Check the system for an nvidia gpu and return whether one is available.""" 8 | try: 9 | nvmlInit() 10 | device_count = nvmlDeviceGetCount() 11 | nvmlShutdown() 12 | return device_count > 0 13 | except Exception: # exception could occur if there is a driver issue, for example 14 | return False 15 | -------------------------------------------------------------------------------- /src/openlifu/util/dict_conversion.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import asdict, dataclass, fields 4 | from typing import Any, Dict, Type, TypeVar, get_origin 5 | 6 | import numpy as np 7 | 8 | T = TypeVar('T', bound='DictMixin') 9 | 10 | @dataclass 11 | class DictMixin: 12 | """Mixin for basic conversion of a dataclass to and from dict.""" 13 | def to_dict(self) -> Dict[str,Any]: 14 | """ 15 | Convert the object to a dictionary 16 | 17 | Returns: Dictionary of object parameters 18 | """ 19 | return asdict(self) 20 | 21 | @classmethod 22 | def from_dict(cls : Type[T], parameter_dict:Dict[str,Any]) -> T: 23 | """ 24 | Create an object from a dictionary 25 | 26 | Args: 27 | parameter_dict: dictionary of parameters to define the object 28 | Returns: new object 29 | """ 30 | if "class" in parameter_dict: 31 | parameter_dict.pop("class") 32 | new_object = cls(**parameter_dict) 33 | 34 | # Convert anything that should be a numpy array to numpy 35 | for field in fields(cls): 36 | # Note that sometimes "field.type" is a string rather than a type due to the "from annotations import __future__" stuff 37 | if get_origin(field.type) is np.ndarray or field.type is np.ndarray or "np.ndarray" in field.type: 38 | setattr(new_object, field.name, np.array(getattr(new_object,field.name))) 39 | 40 | return new_object 41 | -------------------------------------------------------------------------------- /src/openlifu/util/json.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | from datetime import datetime 5 | from pathlib import Path 6 | 7 | import numpy as np 8 | 9 | from openlifu.db.subject import Subject 10 | from openlifu.geo import Point 11 | from openlifu.plan.solution_analysis import SolutionAnalysisOptions 12 | from openlifu.seg.material import Material 13 | from openlifu.xdc.element import Element 14 | from openlifu.xdc.transducer import Transducer 15 | 16 | 17 | class PYFUSEncoder(json.JSONEncoder): 18 | def default(self, obj): 19 | if isinstance(obj, np.integer): 20 | return int(obj) 21 | if isinstance(obj, np.floating): 22 | return float(obj) 23 | if isinstance(obj, np.ndarray): 24 | return obj.tolist() 25 | if isinstance(obj, datetime): 26 | return obj.isoformat() 27 | if isinstance(obj, Point): 28 | return obj.to_dict() 29 | if isinstance(obj, Transducer): 30 | return obj.to_dict() 31 | if isinstance(obj, Element): 32 | return obj.to_dict() 33 | if isinstance(obj, Material): 34 | return obj.to_dict() 35 | if isinstance(obj, Subject): 36 | return obj.to_dict() 37 | if isinstance(obj, SolutionAnalysisOptions): 38 | return obj.to_dict() 39 | return super().default(obj) 40 | 41 | def to_json(obj, filename): 42 | dirname = Path(filename).parent 43 | if dirname and not dirname.exists(): 44 | dirname.mkdir(parents=True) 45 | with open(filename, 'w') as file: 46 | json.dump(obj, file, cls=PYFUSEncoder, indent=4) 47 | -------------------------------------------------------------------------------- /src/openlifu/util/strings.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | from typing import Literal 5 | 6 | Cases = Literal['lower', 'upper', 'same', 'snake', 'camel', 'pascal', 'cobra', 'title', 'sentence'] 7 | 8 | def sanitize(in_str, case: Cases ='snake'): 9 | stripped = ''.join(re.findall(r'[\w\s]+', in_str)) 10 | words = re.findall(r'\w+', stripped) 11 | 12 | if case.lower() == 'lower': 13 | out_str = ''.join(words).lower() 14 | elif case.lower() == 'upper': 15 | out_str = ''.join(words).upper() 16 | elif case.lower() == 'same': 17 | out_str = ''.join(words) 18 | elif case.lower() == 'snake': 19 | out_str = '_'.join(words).lower() 20 | elif case.lower() == 'camel': 21 | out_str = ''.join([word[0].upper() + word[1:].lower() for word in words]) 22 | out_str = out_str[0].lower() + out_str[1:] 23 | elif case.lower() == 'pascal': 24 | out_str = ''.join([word[0].upper() + word[1:].lower() for word in words]) 25 | elif case.lower() == 'cobra': 26 | out_str = '_'.join([word[0].upper() + word[1:].lower() for word in words]) 27 | elif case.lower() == 'title': 28 | words = re.findall(r'\w+', stripped.replace("_", " ")) 29 | words = [word[0].upper() + word[1:].lower() for word in words] 30 | lowercase_words = ["a","an","and","for","in","of","the"] 31 | words = [word.lower() if word in lowercase_words else word for word in words] 32 | out_str = ' '.join(words) 33 | elif case.lower() == 'sentence': 34 | words = re.findall(r'\w+', stripped.replace("_", " ")) 35 | first_word = words[0] 36 | words[0] = first_word[0].upper() + first_word[1:].lower() 37 | out_str = ' '.join(words) 38 | else: 39 | raise ValueError(f'Unrecognized case type {case}') 40 | 41 | return out_str 42 | -------------------------------------------------------------------------------- /src/openlifu/xdc/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from . import element, transducer 4 | from .element import Element 5 | from .transducer import Transducer 6 | 7 | __all__ = [ 8 | "element", 9 | "transducer", 10 | "Element", 11 | "Transducer", 12 | ] 13 | -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | """Helper utilities for unit tests""" 2 | from __future__ import annotations 3 | 4 | from dataclasses import fields, is_dataclass 5 | 6 | import numpy as np 7 | import xarray 8 | 9 | 10 | def dataclasses_are_equal(obj1, obj2) -> bool: 11 | """Return whether two nested dataclass structures are equal by recursively checking for equality of fields, 12 | while specially handling numpy arrays and xarray Datasets. 13 | 14 | Recurses into dataclasses as well as dictionary-like, list-like, and tuple-like fields. 15 | """ 16 | obj_type = type(obj1) 17 | if type(obj2) != obj_type: 18 | return False 19 | elif is_dataclass(obj_type): 20 | return all( 21 | dataclasses_are_equal(getattr(obj1, f.name), getattr(obj2, f.name)) 22 | for f in fields(obj_type) 23 | ) 24 | # handle the builtin types first for speed; subclasses handled below 25 | elif issubclass(obj_type, list) or issubclass(obj_type, tuple): 26 | return all(dataclasses_are_equal(v1,v2) for v1,v2 in zip(obj1,obj2)) 27 | elif issubclass(obj_type, dict): 28 | if obj1.keys() != obj2.keys(): 29 | return False 30 | return all(dataclasses_are_equal(obj1[k],obj2[k]) for k in obj1) 31 | elif issubclass(obj_type, np.ndarray): 32 | return (obj1==obj2).all() 33 | elif issubclass(obj_type, xarray.Dataset): 34 | return obj1.equals(obj2) 35 | else: 36 | return obj1 == obj2 37 | -------------------------------------------------------------------------------- /tests/resources/example_db/protocols/example_protocol/example_protocol.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "example_protocol", 3 | "name": "Example protocol", 4 | "description": "Example protocol created 30-Jan-2024 09:16:02", 5 | "allowed_roles": ["operator"], 6 | "pulse": { 7 | "frequency": 500000, 8 | "amplitude": 1, 9 | "duration": 2e-5, 10 | "class": "Pulse" 11 | }, 12 | "sequence": { 13 | "pulse_interval": 0.1, 14 | "pulse_count": 10, 15 | "pulse_train_interval": 1, 16 | "pulse_train_count": 1 17 | }, 18 | "focal_pattern": { 19 | "target_pressure": 1.0e6, 20 | "units": "Pa", 21 | "class": "SinglePoint" 22 | }, 23 | "delay_method": { 24 | "c0": 1540, 25 | "class": "Direct" 26 | }, 27 | "apod_method": { 28 | "class": "Uniform" 29 | }, 30 | "seg_method": { 31 | "class": "UniformWater", 32 | "materials": { 33 | "water": { 34 | "name": "water", 35 | "sound_speed": 1500, 36 | "density": 1000, 37 | "attenuation": 0.0022, 38 | "specific_heat": 4182, 39 | "thermal_conductivity": 0.598 40 | }, 41 | "tissue": { 42 | "name": "tissue", 43 | "sound_speed": 1540, 44 | "density": 1050, 45 | "attenuation": 0.3, 46 | "specific_heat": 3600, 47 | "thermal_conductivity": 0.528 48 | }, 49 | "skull": { 50 | "name": "skull", 51 | "sound_speed": 2800, 52 | "density": 1900, 53 | "attenuation": 6, 54 | "specific_heat": 1300, 55 | "thermal_conductivity": 0.4 56 | }, 57 | "standoff": { 58 | "name": "standoff", 59 | "sound_speed": 1420, 60 | "density": 1000, 61 | "attenuation": 1, 62 | "specific_heat": 4182, 63 | "thermal_conductivity": 0.598 64 | }, 65 | "air": { 66 | "name": "air", 67 | "sound_speed": 344, 68 | "density": 1.25, 69 | "attenuation": 7.5, 70 | "specific_heat": 1012, 71 | "thermal_conductivity": 0.025 72 | } 73 | }, 74 | "ref_material": "water" 75 | }, 76 | "sim_setup": { 77 | "dims": ["lat", "ele", "ax"], 78 | "names": ["Lateral", "Elevation", "Axial"], 79 | "spacing": 1, 80 | "units": "mm", 81 | "x_extent": [-30, 30], 82 | "y_extent": [-30, 30], 83 | "z_extent": [-4, 70], 84 | "dt": 0, 85 | "t_end": 0, 86 | "options": {} 87 | }, 88 | "param_constraints": { 89 | "MI": { "operator": "<", "error_value": 1.9 } 90 | }, 91 | "target_constraints": [], 92 | "virtual_fit_options": { 93 | "units": "mm", 94 | "transducer_steering_center_distance": 50.0, 95 | "steering_limits": [ 96 | [-49, 47.5], 97 | [-51.2, 53], 98 | [-55, 58] 99 | ], 100 | "pitch_range": [-1, 120], 101 | "pitch_step": 3, 102 | "yaw_range": [-60, 66], 103 | "yaw_step": 2, 104 | "planefit_dyaw_extent": 14, 105 | "planefit_dyaw_step": 2, 106 | "planefit_dpitch_extent": 16, 107 | "planefit_dpitch_step": 7 108 | }, 109 | "analysis_options": { 110 | "standoff_sound_speed": 1500.0, 111 | "standoff_density": 1000.0, 112 | "ref_sound_speed": 1500.0, 113 | "ref_density": 1000.0, 114 | "mainlobe_aspect_ratio": [1.0, 1.0, 5.0], 115 | "mainlobe_radius": 2.5e-3, 116 | "beamwidth_radius": 5e-3, 117 | "sidelobe_radius": 3e-3, 118 | "sidelobe_zmin": 1e-3, 119 | "distance_units": "m" 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /tests/resources/example_db/protocols/protocols.json: -------------------------------------------------------------------------------- 1 | { 2 | "protocol_ids": ["example_protocol"] 3 | } 4 | -------------------------------------------------------------------------------- /tests/resources/example_db/subjects/example_subject/example_subject.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "example_subject", 3 | "name": "Example Subject", 4 | "attrs": {} 5 | } 6 | -------------------------------------------------------------------------------- /tests/resources/example_db/subjects/example_subject/sessions/example_session/example_session.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "example_session", 3 | "subject_id": "example_subject", 4 | "name": "Example Session", 5 | "date_created": "2024-04-09 11:27:30", 6 | "targets": { 7 | "id": "example_target", 8 | "name": "Example Target", 9 | "color": [1, 0, 0], 10 | "radius": 1, 11 | "position": [0, -50, 0], 12 | "dims": ["L", "P", "S"], 13 | "units": "mm" 14 | }, 15 | "markers": [], 16 | "volume_id": "example_volume", 17 | "transducer_id": "example_transducer", 18 | "protocol_id": "example_protocol", 19 | "array_transform": { 20 | "matrix": [ 21 | [-1, 0, 0, 0], 22 | [0, 0.05, 0.998749217771909, -105], 23 | [0, 0.998749217771909, -0.05, 5], 24 | [0, 0, 0, 1] 25 | ], 26 | "units": "mm" 27 | }, 28 | "attrs": {}, 29 | "date_modified": "2024-04-09 11:27:30", 30 | "virtual_fit_results": { 31 | "example_target": [ 32 | [ 33 | true, 34 | { 35 | "matrix": [ 36 | [1.1, 0, 0, 0], 37 | [0, 1.2, 0, 0], 38 | [0, 0, 1.3, 0], 39 | [0, 0.05, 0, 1.4] 40 | ], 41 | "units": "mm" 42 | } 43 | ] 44 | ] 45 | }, 46 | "transducer_tracking_results": [ 47 | { 48 | "photoscan_id": "example_photoscan", 49 | "transducer_to_volume_transform": { 50 | "matrix": [ 51 | [2.0, 0.5, 0.0, 0.0], 52 | [-0.5, 2.0, 0.0, 0.0], 53 | [0.0, 0.0, 1.0, 0.0], 54 | [0.0, 0.0, 0.0, 1.0] 55 | ], 56 | "units": "mm" 57 | }, 58 | "photoscan_to_volume_transform": { 59 | "matrix": [ 60 | [1.0, 0.0, 3.0, 0.0], 61 | [0.0, 1.0, 2.0, 0.0], 62 | [0.0, 0.0, 1.0, 0.0], 63 | [0.0, 0.0, 0.0, 1.0] 64 | ], 65 | "units": "mm" 66 | }, 67 | "transducer_to_volume_tracking_approved": false, 68 | "photoscan_to_volume_tracking_approved": false 69 | } 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /tests/resources/example_db/subjects/example_subject/sessions/example_session/photoscans/example_photoscan/example_photoscan.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "example_photoscan", 3 | "name": "ExamplePhotoscan", 4 | "model_filename": "example_photoscan.obj", 5 | "texture_filename": "example_photoscan_texture.exr", 6 | "photoscan_approved": false 7 | } 8 | -------------------------------------------------------------------------------- /tests/resources/example_db/subjects/example_subject/sessions/example_session/photoscans/example_photoscan/example_photoscan.obj: -------------------------------------------------------------------------------- 1 | # Blender 4.3.0 2 | # www.blender.org 3 | o Cube 4 | v 1.000000 1.000000 -1.000000 5 | v 1.000000 -1.000000 -1.000000 6 | v 1.000000 1.000000 1.000000 7 | v 1.000000 -1.000000 1.000000 8 | v -1.000000 1.000000 -1.000000 9 | v -1.000000 -1.000000 -1.000000 10 | v -1.000000 1.000000 1.000000 11 | v -1.000000 -1.000000 1.000000 12 | vn -0.0000 1.0000 -0.0000 13 | vn -0.0000 -0.0000 1.0000 14 | vn -1.0000 -0.0000 -0.0000 15 | vn -0.0000 -1.0000 -0.0000 16 | vn 1.0000 -0.0000 -0.0000 17 | vn -0.0000 -0.0000 -1.0000 18 | vt 0.625000 0.500000 19 | vt 0.875000 0.500000 20 | vt 0.875000 0.750000 21 | vt 0.625000 0.750000 22 | vt 0.375000 0.750000 23 | vt 0.625000 1.000000 24 | vt 0.375000 1.000000 25 | vt 0.375000 0.000000 26 | vt 0.625000 0.000000 27 | vt 0.625000 0.250000 28 | vt 0.375000 0.250000 29 | vt 0.125000 0.500000 30 | vt 0.375000 0.500000 31 | vt 0.125000 0.750000 32 | s 0 33 | f 1/1/1 5/2/1 7/3/1 3/4/1 34 | f 4/5/2 3/4/2 7/6/2 8/7/2 35 | f 8/8/3 7/9/3 5/10/3 6/11/3 36 | f 6/12/4 2/13/4 4/5/4 8/14/4 37 | f 2/13/5 1/1/5 3/4/5 4/5/5 38 | f 6/11/6 5/10/6 1/1/6 2/13/6 39 | -------------------------------------------------------------------------------- /tests/resources/example_db/subjects/example_subject/sessions/example_session/photoscans/example_photoscan/example_photoscan_texture.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenwaterHealth/OpenLIFU-python/c83a1337118e78051dc79b6c68eaef3d5f781ac9/tests/resources/example_db/subjects/example_subject/sessions/example_session/photoscans/example_photoscan/example_photoscan_texture.exr -------------------------------------------------------------------------------- /tests/resources/example_db/subjects/example_subject/sessions/example_session/photoscans/photoscans.json: -------------------------------------------------------------------------------- 1 | { "photoscan_ids": ["example_photoscan"] } 2 | -------------------------------------------------------------------------------- /tests/resources/example_db/subjects/example_subject/sessions/example_session/runs/example_run/example_run.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "example_run", 3 | "success_flag": true, 4 | "note": "Test note", 5 | "session_id": "example_session", 6 | "solution_id": null 7 | } 8 | -------------------------------------------------------------------------------- /tests/resources/example_db/subjects/example_subject/sessions/example_session/runs/example_run/example_run_protocol_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "example_protocol", 3 | "name": "Example protocol", 4 | "description": "Example protocol created 30-Jan-2024 09:16:02", 5 | "pulse": { 6 | "frequency": 500000, 7 | "amplitude": 1.0, 8 | "duration": 2e-5, 9 | "class": "Pulse" 10 | }, 11 | "sequence": { 12 | "pulse_interval": 0.1, 13 | "pulse_count": 10, 14 | "pulse_train_interval": 1, 15 | "pulse_train_count": 1 16 | }, 17 | "focal_pattern": { 18 | "target_pressure": 1.0e6, 19 | "class": "SinglePoint" 20 | }, 21 | "delay_method": { 22 | "c0": 1540, 23 | "class": "Direct" 24 | }, 25 | "apod_method": { 26 | "class": "Uniform" 27 | }, 28 | "seg_method": { 29 | "class": "UniformWater", 30 | "materials": { 31 | "water": { 32 | "name": "water", 33 | "sound_speed": 1500, 34 | "density": 1000, 35 | "attenuation": 0.0022, 36 | "specific_heat": 4182, 37 | "thermal_conductivity": 0.598 38 | }, 39 | "tissue": { 40 | "name": "tissue", 41 | "sound_speed": 1540, 42 | "density": 1050, 43 | "attenuation": 0.3, 44 | "specific_heat": 3600, 45 | "thermal_conductivity": 0.528 46 | }, 47 | "skull": { 48 | "name": "skull", 49 | "sound_speed": 2800, 50 | "density": 1900, 51 | "attenuation": 6, 52 | "specific_heat": 1300, 53 | "thermal_conductivity": 0.4 54 | }, 55 | "standoff": { 56 | "name": "standoff", 57 | "sound_speed": 1420, 58 | "density": 1000, 59 | "attenuation": 1, 60 | "specific_heat": 4182, 61 | "thermal_conductivity": 0.598 62 | }, 63 | "air": { 64 | "name": "air", 65 | "sound_speed": 344, 66 | "density": 1.25, 67 | "attenuation": 7.5, 68 | "specific_heat": 1012, 69 | "thermal_conductivity": 0.025 70 | } 71 | }, 72 | "ref_material": "water" 73 | }, 74 | "sim_setup": { 75 | "dims": ["lat", "ele", "ax"], 76 | "names": ["Lateral", "Elevation", "Axial"], 77 | "spacing": 1, 78 | "units": "mm", 79 | "x_extent": [-30, 30], 80 | "y_extent": [-30, 30], 81 | "z_extent": [-4, 70], 82 | "dt": 0, 83 | "t_end": 0, 84 | "options": {} 85 | }, 86 | "param_constraints": {}, 87 | "target_constraints": {}, 88 | "analysis_options": {} 89 | } 90 | -------------------------------------------------------------------------------- /tests/resources/example_db/subjects/example_subject/sessions/example_session/runs/example_run/example_run_session_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "example_session", 3 | "subject_id": "example_subject", 4 | "name": "Example Session", 5 | "date_created": "2024-04-09 11:27:30", 6 | "targets": { 7 | "id": "example_target", 8 | "name": "Example Target", 9 | "color": [1, 0, 0], 10 | "radius": 1, 11 | "position": [0, -50, 0], 12 | "dims": ["L", "P", "S"], 13 | "units": "mm" 14 | }, 15 | "markers": [], 16 | "volume_id": "example_volume", 17 | "transducer_id": "example_transducer", 18 | "protocol_id": "example_protocol", 19 | "array_transform": { 20 | "matrix": [ 21 | [-1, 0, 0, 0], 22 | [0, 0.05, 0.998749217771909, -105], 23 | [0, 0.998749217771909, -0.05, 5], 24 | [0, 0, 0, 1] 25 | ], 26 | "units": "mm" 27 | }, 28 | "attrs": {}, 29 | "date_modified": "2024-04-09 11:27:30" 30 | } 31 | -------------------------------------------------------------------------------- /tests/resources/example_db/subjects/example_subject/sessions/example_session/runs/runs.json: -------------------------------------------------------------------------------- 1 | { 2 | "run_ids": ["example_run"] 3 | } 4 | -------------------------------------------------------------------------------- /tests/resources/example_db/subjects/example_subject/sessions/example_session/solutions/example_solution/example_solution.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "example_solution", 3 | "name": "Example Solution", 4 | "protocol_id": "example_protocol", 5 | "transducer_id": "example_transducer", 6 | "date_created": "2024-01-30T09:18:11", 7 | "description": "Example plan created 30-Jan-2024 09:16:02", 8 | "delays": [ 9 | [ 10 | 7.139258974920841e-7, 1.164095583074107e-6, 1.4321977043056202e-6, 11 | 1.5143736925332023e-6, 1.4094191657896087e-6, 1.1188705305994071e-6, 12 | 6.46894718323139e-7, 0.0, 1.2683760653622907e-6, 1.7251583088871662e-6, 13 | 1.9972746364594766e-6, 2.0806926330138847e-6, 1.974152793990993e-6, 14 | 1.6792617729461087e-6, 1.2003736682835394e-6, 5.442792226777689e-7, 15 | 1.6425243839760022e-6, 2.1038804054306437e-6, 2.378775260158848e-6, 16 | 2.4630532471222807e-6, 2.3554157329476133e-6, 2.0575192329568598e-6, 17 | 1.5738505533232074e-6, 9.114003043615498e-7, 1.8309933020916417e-6, 18 | 2.29468834620898e-6, 2.571004767271362e-6, 2.655722845121427e-6, 19 | 2.5475236155907678e-6, 2.248089500472459e-6, 1.7619762095269915e-6, 20 | 1.0962779929139644e-6, 1.8309933020916417e-6, 2.29468834620898e-6, 21 | 2.571004767271362e-6, 2.655722845121427e-6, 2.5475236155907678e-6, 22 | 2.248089500472459e-6, 1.7619762095269915e-6, 1.0962779929139644e-6, 23 | 1.6425243839760022e-6, 2.1038804054306437e-6, 2.378775260158848e-6, 24 | 2.4630532471222807e-6, 2.3554157329476133e-6, 2.0575192329568598e-6, 25 | 1.5738505533232074e-6, 9.114003043615498e-7, 1.2683760653622907e-6, 26 | 1.7251583088871662e-6, 1.9972746364594766e-6, 2.0806926330138847e-6, 27 | 1.974152793990993e-6, 1.6792617729461087e-6, 1.2003736682835394e-6, 28 | 5.442792226777689e-7, 7.139258974920841e-7, 1.164095583074107e-6, 29 | 1.4321977043056202e-6, 1.5143736925332023e-6, 1.4094191657896087e-6, 30 | 1.1188705305994071e-6, 6.46894718323139e-7, 0.0 31 | ] 32 | ], 33 | "apodizations": [ 34 | [ 35 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 36 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 37 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 38 | ] 39 | ], 40 | "pulse": { 41 | "frequency": 500000, 42 | "amplitude": 1.0, 43 | "duration": 2e-5 44 | }, 45 | "voltage": 2.3167290687561035, 46 | "sequence": { 47 | "pulse_interval": 0.1, 48 | "pulse_count": 10, 49 | "pulse_train_interval": 1, 50 | "pulse_train_count": 1 51 | }, 52 | "foci": [ 53 | { 54 | "id": "example_target", 55 | "name": "Example Target", 56 | "color": [1.0, 0.0, 0.0], 57 | "radius": 0.001, 58 | "position": [0.0, -0.0022437460888595447, 0.05518120697745499], 59 | "dims": ["lat", "ele", "ax"], 60 | "units": "m" 61 | } 62 | ], 63 | "target": { 64 | "id": "example_target", 65 | "name": "Example Target", 66 | "color": [1.0, 0.0, 0.0], 67 | "radius": 0.001, 68 | "position": [0.0, -0.0022437460888595447, 0.05518120697745499], 69 | "dims": ["lat", "ele", "ax"], 70 | "units": "m" 71 | }, 72 | "approved": false 73 | } 74 | -------------------------------------------------------------------------------- /tests/resources/example_db/subjects/example_subject/sessions/example_session/solutions/example_solution/example_solution.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenwaterHealth/OpenLIFU-python/c83a1337118e78051dc79b6c68eaef3d5f781ac9/tests/resources/example_db/subjects/example_subject/sessions/example_session/solutions/example_solution/example_solution.nc -------------------------------------------------------------------------------- /tests/resources/example_db/subjects/example_subject/sessions/example_session/solutions/example_solution/example_solution_analysis.json: -------------------------------------------------------------------------------- 1 | { 2 | "mainlobe_pnp_MPa": 0.99999994, 3 | "mainlobe_isppa_Wcm2": 33.3333321, 4 | "mainlobe_ispta_mWcm2": 6.66666651, 5 | "beamwidth_lat_3dB_mm": 4.56580212813179, 6 | "beamwidth_ax_3dB_mm": 36.010904382076696, 7 | "beamwidth_lat_6dB_mm": 6.6535180608625621, 8 | "beamwidth_ax_6dB_mm": 39.130310962465927, 9 | "sidelobe_pnp_MPa": 0.928957939, 10 | "sidelobe_isppa_Wcm2": 28.7654324, 11 | "global_pnp_MPa": 0.99999994, 12 | "global_isppa_Wcm2": 33.3333321, 13 | "p0_MPa": 0.23167290688, 14 | "TIC": 0.022197405589496896, 15 | "power_W": 0.0028052740834448721, 16 | "MI": 1.41421342, 17 | "global_ispta_mWcm2": 6.66666651 18 | } 19 | -------------------------------------------------------------------------------- /tests/resources/example_db/subjects/example_subject/sessions/example_session/solutions/solutions.json: -------------------------------------------------------------------------------- 1 | { 2 | "solution_ids": ["example_solution"] 3 | } 4 | -------------------------------------------------------------------------------- /tests/resources/example_db/subjects/example_subject/sessions/sessions.json: -------------------------------------------------------------------------------- 1 | { 2 | "session_ids": ["example_session"] 3 | } 4 | -------------------------------------------------------------------------------- /tests/resources/example_db/subjects/example_subject/volumes/example_volume/example_volume.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "example_volume", 3 | "name": "EXAMPLE_VOLUME", 4 | "data_filename": "example_volume.nii" 5 | } 6 | -------------------------------------------------------------------------------- /tests/resources/example_db/subjects/example_subject/volumes/example_volume/example_volume.nii: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenwaterHealth/OpenLIFU-python/c83a1337118e78051dc79b6c68eaef3d5f781ac9/tests/resources/example_db/subjects/example_subject/volumes/example_volume/example_volume.nii -------------------------------------------------------------------------------- /tests/resources/example_db/subjects/example_subject/volumes/volumes.json: -------------------------------------------------------------------------------- 1 | { 2 | "volume_ids": ["example_volume"] 3 | } 4 | -------------------------------------------------------------------------------- /tests/resources/example_db/subjects/subjects.json: -------------------------------------------------------------------------------- 1 | { 2 | "subject_ids": ["example_subject"] 3 | } 4 | -------------------------------------------------------------------------------- /tests/resources/example_db/systems/systems.json: -------------------------------------------------------------------------------- 1 | { 2 | "system_ids": [] 3 | } 4 | -------------------------------------------------------------------------------- /tests/resources/example_db/transducers/transducers.json: -------------------------------------------------------------------------------- 1 | { "transducer_ids": ["example_transducer"] } 2 | -------------------------------------------------------------------------------- /tests/resources/example_db/users/example_user/example_user.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "example_user", 3 | "password_hash": "example_pw_hash", 4 | "roles": ["example_role_1", "example_role_2"], 5 | "name": "Example User", 6 | "description": "This user is an example." 7 | } 8 | -------------------------------------------------------------------------------- /tests/resources/example_db/users/users.json: -------------------------------------------------------------------------------- 1 | { "user_ids": ["example_user"] } 2 | -------------------------------------------------------------------------------- /tests/test_apod_methods.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from openlifu.bf.apod_methods import MaxAngle, PiecewiseLinear, Uniform 6 | 7 | 8 | # Test apodization methods with default parameters 9 | @pytest.mark.parametrize("method_class", [Uniform, MaxAngle, PiecewiseLinear]) 10 | def test_apodization_methods_default_params(method_class): 11 | method = method_class() 12 | assert isinstance(method, method_class) 13 | 14 | # Test apodization methods with custom parameters 15 | @pytest.mark.parametrize(("method_class", "params"), [ 16 | (Uniform, {}), 17 | (MaxAngle, {"max_angle": 45.0}), 18 | (PiecewiseLinear, {"zero_angle": 90.0, "rolloff_angle": 30.0}), 19 | ]) 20 | def test_apodization_methods_custom_params(method_class, params): 21 | method = method_class(**params) 22 | assert isinstance(method, method_class) 23 | for key, value in params.items(): 24 | assert getattr(method, key) == value 25 | 26 | # Test apodization methods with invalid parameters 27 | @pytest.mark.parametrize(("method_class","invalid_params"), [ 28 | (MaxAngle, {"max_angle": -10.0}), 29 | (PiecewiseLinear, {"zero_angle": 30.0, "rolloff_angle": 45.0}), 30 | ]) 31 | def test_apodization_methods_invalid_params(method_class, invalid_params): 32 | with pytest.raises((TypeError, ValueError)): 33 | method_class(**invalid_params) 34 | 35 | # Test apodization methods with non-numeric parameters 36 | @pytest.mark.parametrize(("method_class","invalid_params"), [ 37 | (MaxAngle, {"max_angle": "invalid"}), 38 | (PiecewiseLinear, {"zero_angle": "invalid", "rolloff_angle": "invalid"}), 39 | ]) 40 | def test_apodization_methods_non_numeric_params(method_class, invalid_params): 41 | with pytest.raises(TypeError): 42 | method_class(**invalid_params) 43 | -------------------------------------------------------------------------------- /tests/test_geo.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | 5 | from openlifu.geo import ( 6 | Point, 7 | cartesian_to_spherical, 8 | cartesian_to_spherical_vectorized, 9 | create_standoff_transform, 10 | spherical_coordinate_basis, 11 | spherical_to_cartesian, 12 | spherical_to_cartesian_vectorized, 13 | ) 14 | 15 | 16 | def test_point_from_dict(): 17 | point = Point.from_dict({'position' : [10,20,30],}) 18 | assert (point.position == np.array([10,20,30], dtype=float)).all() 19 | 20 | def test_spherical_coordinate_range(): 21 | """Verify that spherical coordinate output is in the prescribed value ranges""" 22 | rng = np.random.default_rng(848) 23 | # try all 8 octants of 3D space 24 | for sign_x in [-1,1]: 25 | for sign_y in [-1,1]: 26 | for sign_z in [-1,1]: 27 | cartesian_coords = np.array([sign_x, sign_y, sign_z]) * rng.random(size=3) 28 | r, th, ph = cartesian_to_spherical(*cartesian_coords) 29 | assert r>=0 30 | assert 0 <= th <= np.pi 31 | assert -np.pi <= ph <= np.pi 32 | 33 | def test_spherical_coordinate_conversion_inverse(): 34 | """Verify that the spherical coordinate conversion forward and backward functions are inverses of one another""" 35 | rng = np.random.default_rng(241) 36 | # try all 8 octants of 3D space 37 | for sign_x in [-1,1]: 38 | for sign_y in [-1,1]: 39 | for sign_z in [-1,1]: 40 | cartesian_coords = np.array([sign_x, sign_y, sign_z]) * rng.random(size=3) 41 | np.testing.assert_almost_equal( 42 | spherical_to_cartesian(*cartesian_to_spherical(*cartesian_coords)), 43 | cartesian_coords 44 | ) 45 | np.testing.assert_almost_equal( 46 | cartesian_to_spherical(*spherical_to_cartesian(*cartesian_to_spherical(*cartesian_coords))), 47 | cartesian_to_spherical(*cartesian_coords) 48 | ) 49 | 50 | def test_cartesian_to_spherical_vectorized(): 51 | rng = np.random.default_rng(35932) 52 | points_cartesian = rng.normal(size=(10,3), scale=2) # make 10 random cartesian points 53 | points_spherical = cartesian_to_spherical_vectorized(points_cartesian) 54 | # Check individual points against the non-vectorized conversion function: 55 | for point_cartesian, point_spherical in zip(points_cartesian, points_spherical): 56 | assert np.allclose( 57 | point_spherical, # result of vectorized converter 58 | np.array(cartesian_to_spherical(*point_cartesian)), # non-vectorized converter 59 | ) 60 | 61 | def test_spherical_to_cartesian_vectorized(): 62 | rng = np.random.default_rng(85932) 63 | 64 | # make 10 random points in spherical coordinates 65 | num_pts = 10 66 | points_spherical = np.zeros(shape=(num_pts,3)) 67 | points_spherical[...,0] = rng.random(num_pts)*5 # random r coordinates 68 | points_spherical[...,1] = rng.random(num_pts)*np.pi # random theta coordinates 69 | points_spherical[...,2] = rng.random(num_pts)*2*np.pi-np.pi # random phi coordinates 70 | 71 | points_cartesian = spherical_to_cartesian_vectorized(points_spherical) 72 | # Check individual points against the non-vectorized conversion function: 73 | for point_cartesian, point_spherical in zip(points_cartesian, points_spherical): 74 | assert np.allclose( 75 | point_cartesian, # result of vectorized converter 76 | np.array(spherical_to_cartesian(*point_spherical)), # non-vectorized converter 77 | ) 78 | 79 | def test_spherical_coordinate_basis(): 80 | rng = np.random.default_rng(35235) 81 | th = rng.random()*np.pi 82 | phi = rng.random()*2*np.pi-np.pi 83 | r = rng.random()*10 84 | basis = spherical_coordinate_basis(th,phi) 85 | assert np.allclose(basis @ basis.T, np.eye(3)) # verify it is an orthonormal basis 86 | r_hat, theta_hat, phi_hat = basis 87 | point = np.array(spherical_to_cartesian(r, th, phi)) 88 | assert np.allclose(np.diff(r_hat / point), 0) # verify that r_hat is a scalar multiple of the cartesian coords 89 | assert cartesian_to_spherical_vectorized(point + 0.01*phi_hat)[2] > phi # verify phi_hat points along increasing phi 90 | assert cartesian_to_spherical_vectorized(point + 0.01*theta_hat)[1] > th # verify theta_hat points along increasing theta 91 | 92 | def test_create_standoff_transform(): 93 | z_offset = 3.2 94 | dzdy = 0.15 95 | t = create_standoff_transform(z_offset, dzdy) 96 | assert np.allclose(t[:3,:3] @ t[:3,:3].T, np.eye(3)) # it's an orthonormal transform 97 | assert np.allclose(np.linalg.det(t[:3,:3]), 1.0) # orientation preserving 98 | assert np.allclose(t @ np.array([0,0,0,1]), np.array([0,0,-z_offset,1.])) # translates the origin correctly 99 | new_x_axis = (t @ np.array([1,0,0,1]) - t @ np.array([0,0,0,1]))[:3] 100 | new_y_axis = (t @ np.array([0,1,0,1]) - t @ np.array([0,0,0,1]))[:3] 101 | assert np.allclose(new_x_axis, np.array([1.,0,0])) 102 | assert new_y_axis[2] > 0 # the y axis was rotated upward, so that the top of the transducer gets closer to the skin 103 | -------------------------------------------------------------------------------- /tests/test_io.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | import pytest 6 | 7 | from openlifu.io import LIFUInterface 8 | from openlifu.plan.solution import Solution 9 | 10 | 11 | # Test LIFUInterface in test_mode 12 | @pytest.fixture() 13 | def lifu_interface(): 14 | interface = LIFUInterface(TX_test_mode=True, HV_test_mode=True, run_async=False) 15 | assert isinstance(interface, LIFUInterface) 16 | yield interface 17 | interface.close() 18 | 19 | # load example solution 20 | @pytest.fixture() 21 | def example_solution(): 22 | return Solution.from_files(Path(__file__).parent/"resources/example_db/subjects/example_subject/sessions/example_session/solutions/example_solution/example_solution.json") 23 | 24 | # Test LIFUInterface with example solution 25 | def test_lifu_interface_with_solution(lifu_interface, example_solution): 26 | assert all(lifu_interface.is_device_connected()) 27 | # Load the example solution 28 | lifu_interface.set_solution(example_solution) 29 | 30 | # Create invalid duty cycle solution 31 | @pytest.fixture() 32 | def invalid_duty_cycle_solution(): 33 | solution = Solution.from_files(Path(__file__).parent/"resources/example_db/subjects/example_subject/sessions/example_session/solutions/example_solution/example_solution.json") 34 | solution.pulse.duration = 0.06 35 | solution.sequence.pulse_interval = 0.1 36 | return solution 37 | 38 | # Test LIFUInterface with invalid solution 39 | def test_lifu_interface_with_invalid_solution(lifu_interface, invalid_duty_cycle_solution): 40 | with pytest.raises(ValueError, match=R"Sequence duty cycle"): 41 | lifu_interface.set_solution(invalid_duty_cycle_solution) 42 | 43 | # Create invalid voltage solution 44 | @pytest.fixture() 45 | def invalid_voltage_solution(): 46 | solution = Solution.from_files(Path(__file__).parent/"resources/example_db/subjects/example_subject/sessions/example_session/solutions/example_solution/example_solution.json") 47 | solution.voltage = 1000 # Set voltage above maximum 48 | return solution 49 | 50 | # Test LIFUInterface with invalid voltage solution 51 | def test_lifu_interface_with_invalid_voltage_solution(lifu_interface, invalid_voltage_solution): 52 | with pytest.raises(ValueError, match=R"exceeds maximum allowed voltage"): 53 | lifu_interface.set_solution(invalid_voltage_solution) 54 | 55 | # Create too long sequence solution 56 | @pytest.fixture() 57 | def too_long_sequence_solution(): 58 | solution = Solution.from_files(Path(__file__).parent/"resources/example_db/subjects/example_subject/sessions/example_session/solutions/example_solution/example_solution.json") 59 | solution.voltage = 40 60 | solution.pulse.duration = 0.05 61 | solution.sequence.pulse_interval = 0.1 62 | solution.sequence.pulse_count = 10 63 | solution.sequence.pulse_train_interval = 1.0 64 | solution.sequence.pulse_train_count = 600 65 | return solution 66 | 67 | # Test LIFUInterface with too long sequence solution 68 | def test_lifu_interface_with_too_long_sequence_solution(lifu_interface, too_long_sequence_solution): 69 | with pytest.raises(ValueError, match=R"exceeds maximum allowed voltage"): 70 | lifu_interface.set_solution(too_long_sequence_solution) 71 | -------------------------------------------------------------------------------- /tests/test_material.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import asdict 4 | 5 | import pytest 6 | 7 | from openlifu.seg.material import Material 8 | 9 | # Mock PARAM_INFO for tests 10 | PARAM_INFO = { 11 | "name": {"label": "Material name", "description": "Name for the material"}, 12 | "sound_speed": {"label": "Sound speed (m/s)", "description": "Speed of sound in the material (m/s)"}, 13 | "density": {"label": "Density (kg/m^3)", "description": "Mass density of the material (kg/m^3)"}, 14 | "attenuation": {"label": "Attenuation (dB/cm/MHz)", "description": "Ultrasound attenuation in the material (dB/cm/MHz)"}, 15 | "specific_heat": {"label": "Specific heat (J/kg/K)", "description": "Specific heat capacity of the material (J/kg/K)"}, 16 | "thermal_conductivity": {"label": "Thermal conductivity (W/m/K)", "description": "Thermal conductivity of the material (W/m/K)"}, 17 | } 18 | 19 | @pytest.fixture() 20 | def default_material(): 21 | return Material() 22 | 23 | def test_default_material_values(default_material): 24 | assert default_material.name == "Material" 25 | assert default_material.sound_speed == 1500.0 26 | assert default_material.density == 1000.0 27 | assert default_material.attenuation == 0.0 28 | assert default_material.specific_heat == 4182.0 29 | assert default_material.thermal_conductivity == 0.598 30 | 31 | def test_material_to_dict(default_material): 32 | expected = asdict(default_material) 33 | assert default_material.to_dict() == expected 34 | 35 | def test_material_from_dict(): 36 | data = { 37 | "name": "test", 38 | "sound_speed": 1234.0, 39 | "density": 999.0, 40 | "attenuation": 1.2, 41 | "specific_heat": 4000.0, 42 | "thermal_conductivity": 0.5 43 | } 44 | material = Material.from_dict(data) 45 | assert material.to_dict() == data 46 | 47 | def test_get_param_valid(default_material): 48 | assert default_material.get_param("density") == 1000.0 49 | 50 | def test_get_param_invalid(default_material): 51 | with pytest.raises(ValueError, match="Parameter fake_param not found."): 52 | default_material.get_param("fake_param") 53 | 54 | def test_param_info_valid(monkeypatch): 55 | monkeypatch.setattr("openlifu.seg.material.PARAM_INFO", PARAM_INFO) 56 | info = Material.param_info("density") 57 | assert info["label"] == "Density (kg/m^3)" 58 | 59 | def test_param_info_invalid(monkeypatch): 60 | monkeypatch.setattr("openlifu.seg.material.PARAM_INFO", PARAM_INFO) 61 | with pytest.raises(ValueError, match="Parameter unknown not found."): 62 | Material.param_info("unknown") 63 | -------------------------------------------------------------------------------- /tests/test_offset_grid.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | import pytest 5 | from xarray import DataArray, Dataset 6 | 7 | from openlifu.plan.solution_analysis import get_offset_grid 8 | 9 | 10 | @pytest.fixture() 11 | def example_xarr() -> DataArray: 12 | rng = np.random.default_rng(147) 13 | return Dataset( 14 | { 15 | 'p': DataArray( 16 | data=rng.random((3, 2, 3)), 17 | dims=["x", "y", "z"], 18 | attrs={'units': "Pa"} 19 | ) 20 | }, 21 | coords={ 22 | 'x': DataArray(dims=["x"], data=np.linspace(0, 1, 3), attrs={'units': "mm"}), 23 | 'y': DataArray(dims=["y"], data=np.linspace(0, 1, 2), attrs={'units': "mm"}), 24 | 'z': DataArray(dims=["z"], data=np.linspace(0, 1, 3), attrs={'units': "mm"}) 25 | } 26 | ) 27 | 28 | def test_offset_grid(example_xarr: Dataset): 29 | """Test that the distance grid from the focus point is correct.""" 30 | expected = np.array([ 31 | [[[ 0. , 0. , -1. ], 32 | [ 0. , 0. , -0.5], 33 | [ 0. , 0. , 0. ]], 34 | 35 | [[ 0. , 1. , -1. ], 36 | [ 0. , 1. , -0.5], 37 | [ 0. , 1. , 0. ]]], 38 | 39 | 40 | [[[ 0.5, 0. , -1. ], 41 | [ 0.5, 0. , -0.5], 42 | [ 0.5, 0. , 0. ]], 43 | 44 | [[ 0.5, 1. , -1. ], 45 | [ 0.5, 1. , -0.5], 46 | [ 0.5, 1. , 0. ]]], 47 | 48 | 49 | [[[ 1. , 0. , -1. ], 50 | [ 1. , 0. , -0.5], 51 | [ 1. , 0. , 0. ]], 52 | 53 | [[ 1. , 1. , -1. ], 54 | [ 1. , 1. , -0.5], 55 | [ 1. , 1. , 0. ]]]]) 56 | offset = get_offset_grid(example_xarr, [0.0, 0.0, 1.0], as_dataset=False) 57 | 58 | np.testing.assert_almost_equal(offset, expected) 59 | -------------------------------------------------------------------------------- /tests/test_package.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import importlib.metadata 4 | 5 | import openlifu as m 6 | 7 | 8 | def test_version(): 9 | assert importlib.metadata.version("openlifu") == m.__version__ 10 | -------------------------------------------------------------------------------- /tests/test_param_constraints.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from openlifu.plan.param_constraint import ParameterConstraint 6 | 7 | # ---- Tests for ParameterConstraint ---- 8 | 9 | @pytest.fixture() 10 | def threshold_constraint(): 11 | return ParameterConstraint(operator="<=", warning_value=5.0, error_value=7.0) 12 | 13 | @pytest.fixture() 14 | def range_constraint(): 15 | return ParameterConstraint(operator="within", warning_value=(2.0, 4.0), error_value=(1.0, 5.0)) 16 | 17 | def test_invalid_no_thresholds(): 18 | with pytest.raises(ValueError, match="At least one of warning_value or error_value must be set"): 19 | ParameterConstraint(operator="<=") 20 | 21 | def test_invalid_warning_tuple_order(): 22 | with pytest.raises(ValueError, match="Warning value must be a sorted tuple"): 23 | ParameterConstraint(operator="within", warning_value=(4.0, 2.0)) 24 | 25 | def test_invalid_error_type(): 26 | with pytest.raises(ValueError, match="Error value must be a single value"): 27 | ParameterConstraint(operator=">", error_value=(1.0, 2.0)) 28 | 29 | @pytest.mark.parametrize(("value", "op", "threshold", "expected"), [ 30 | (3, "<", 5, True), 31 | (5, "<", 5, False), 32 | (5, "<=", 5, True), 33 | (6, ">", 5, True), 34 | (5, ">=", 5, True), 35 | (3, "within", (2, 4), True), 36 | (2, "within", (2, 4), False), 37 | (1, "inside", (2, 4), False), 38 | (2, "inside", (2, 4), True), 39 | (3, "inside", (2, 4), True), 40 | (1, "outside", (2, 4), True), 41 | (2, "outside", (2, 4), False), 42 | (2, "outside_inclusive", (2, 4), True), 43 | (3, "outside_inclusive", (2, 4), False), 44 | ]) 45 | def test_compare(value, op, threshold, expected): 46 | assert ParameterConstraint.compare(value, op, threshold) == expected 47 | 48 | def test_is_warning(threshold_constraint): 49 | assert not threshold_constraint.is_warning(4.0) 50 | assert threshold_constraint.is_warning(6.0) 51 | 52 | def test_is_error(threshold_constraint): 53 | assert not threshold_constraint.is_error(6.0) 54 | assert threshold_constraint.is_error(8.0) 55 | 56 | def test_is_warning_range(range_constraint): 57 | assert not range_constraint.is_warning(3.0) 58 | assert range_constraint.is_warning(4.5) 59 | 60 | def test_is_error_range(range_constraint): 61 | assert not range_constraint.is_error(3.0) 62 | assert range_constraint.is_error(5.5) 63 | 64 | @pytest.mark.parametrize(("value", "expected_status"), [ 65 | (3.0, "ok"), 66 | (6.5, "warning"), 67 | (7.5, "error"), 68 | ]) 69 | def test_get_status_threshold(value, expected_status): 70 | constraint = ParameterConstraint(operator="<=", warning_value=5.5, error_value=7.0) 71 | assert constraint.get_status(value) == expected_status 72 | 73 | @pytest.mark.parametrize(("value", "expected"), [ 74 | (2.5, "ok"), 75 | (0.5, "warning"), 76 | (5.5, "error"), 77 | ]) 78 | def test_get_status_range(value, expected): 79 | constraint = ParameterConstraint(operator="within", warning_value=(1.0, 4.0), error_value=(0.0, 5.0)) 80 | assert constraint.get_status(value) == expected 81 | -------------------------------------------------------------------------------- /tests/test_point.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | import pytest 5 | from helpers import dataclasses_are_equal 6 | 7 | from openlifu import Point 8 | 9 | 10 | @pytest.fixture() 11 | def example_point() -> Point: 12 | return Point( 13 | id = "example_point", 14 | name="Example point", 15 | color=(0.,0.7, 0.2), 16 | radius=1.5, 17 | position=np.array([-10.,0,25]), 18 | dims = ("R", "A", "S"), 19 | units = "m", 20 | ) 21 | 22 | @pytest.mark.parametrize("compact_representation", [True, False]) 23 | @pytest.mark.parametrize("default_point", [True, False]) 24 | def test_serialize_deserialize_point(example_point : Point, compact_representation: bool, default_point: bool): 25 | """Verify that turning a point into json and then re-constructing it gets back to the original point""" 26 | point = Point() if default_point else example_point 27 | reconstructed_point = point.from_json(point.to_json(compact_representation)) 28 | assert dataclasses_are_equal(point, reconstructed_point) 29 | -------------------------------------------------------------------------------- /tests/test_run.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | import pytest 6 | from helpers import dataclasses_are_equal 7 | 8 | from openlifu.plan import Run 9 | 10 | 11 | @pytest.fixture() 12 | def example_run() -> Run: 13 | return Run( 14 | id="example_run", 15 | name="example_run_name", 16 | success_flag = True, 17 | note="Example note", 18 | session_id="example_session", 19 | solution_id="example_solution", 20 | ) 21 | 22 | def test_default_run(): 23 | """Ensure it is possible to construct a default Run""" 24 | Run() 25 | 26 | def test_save_load_run_from_file(example_run:Run, tmp_path:Path): 27 | """Test that a run can be saved to and loaded from disk faithfully.""" 28 | json_filepath = tmp_path/"some_directory"/"example_run.json" 29 | example_run.to_file(json_filepath) 30 | assert dataclasses_are_equal(example_run.from_file(json_filepath), example_run) 31 | 32 | @pytest.mark.parametrize("compact_representation", [True, False]) 33 | def test_save_load_run_from_json(example_run:Run, compact_representation: bool): 34 | """Test that a run can be saved to and loaded from json faithfully.""" 35 | run_json = example_run.to_json(compact = compact_representation) 36 | assert dataclasses_are_equal(example_run.from_json(run_json), example_run) 37 | -------------------------------------------------------------------------------- /tests/test_seg_method.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from openlifu.seg import MATERIALS, Material, SegmentationMethod, seg_methods 6 | from openlifu.seg.seg_methods.uniform import UniformSegmentation 7 | 8 | 9 | @pytest.fixture() 10 | def example_seg_method() -> seg_methods.UniformSegmentation: 11 | return seg_methods.UniformSegmentation( 12 | materials = { 13 | 'water' : Material( 14 | name="water", 15 | sound_speed=1500.0, 16 | density=1000.0, 17 | attenuation=0.0, 18 | specific_heat=4182.0, 19 | thermal_conductivity=0.598 20 | ), 21 | 'skull' : Material( 22 | name="skull", 23 | sound_speed=4080.0, 24 | density=1900.0, 25 | attenuation=0.0, 26 | specific_heat=1100.0, 27 | thermal_conductivity=0.3 28 | ), 29 | }, 30 | ref_material = 'water', 31 | ) 32 | 33 | def test_seg_method_dict_conversion(example_seg_method : seg_methods.UniformSegmentation): 34 | assert SegmentationMethod.from_dict(example_seg_method.to_dict()) == example_seg_method 35 | 36 | def test_seg_method_no_instantiate_abstract_class(): 37 | with pytest.raises(TypeError): 38 | SegmentationMethod() # pyright: ignore[reportAbstractUsage] 39 | 40 | def test_uniform_seg_method_no_reference_material(): 41 | with pytest.raises(ValueError, match="Reference material non_existent_material not found."): 42 | UniformSegmentation( 43 | materials = { 44 | 'water' : Material( 45 | name="water", 46 | sound_speed=1500.0, 47 | density=1000.0, 48 | attenuation=0.0, 49 | specific_heat=4182.0, 50 | thermal_conductivity=0.598 51 | ), 52 | 'skull' : Material( 53 | name="skull", 54 | sound_speed=4080.0, 55 | density=1900.0, 56 | attenuation=0.0, 57 | specific_heat=1100.0, 58 | thermal_conductivity=0.3 59 | ), 60 | }, 61 | ref_material = 'non_existent_material', 62 | ) 63 | 64 | def test_uniformwater_errors_when_specify_ref_material(): 65 | with pytest.raises(TypeError): 66 | seg_methods.UniformWater( 67 | ref_material = 'water', # pyright: ignore[reportCallIssue] 68 | ) 69 | 70 | def test_materials_as_none_gets_default_materials(): 71 | seg_method = seg_methods.UniformSegmentation(materials=None) # pyright: ignore[reportArgumentType] 72 | assert seg_method.materials == MATERIALS.copy() 73 | -------------------------------------------------------------------------------- /tests/test_sequence.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from helpers import dataclasses_are_equal 4 | 5 | from openlifu import Sequence 6 | 7 | 8 | def test_dict_undict_sequence(): 9 | """Test that conversion between Sequence and dict works""" 10 | sequence = Sequence(pulse_interval=2, pulse_count=5, pulse_train_interval=11, pulse_train_count=3) 11 | reconstructed_sequence = Sequence.from_dict(sequence.to_dict()) 12 | assert dataclasses_are_equal(sequence, reconstructed_sequence) 13 | -------------------------------------------------------------------------------- /tests/test_sim.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | 5 | import pytest 6 | import xarray 7 | 8 | import openlifu 9 | 10 | 11 | @pytest.mark.skipif( 12 | sys.platform == 'darwin', 13 | reason=( 14 | "This test is skipped on macOS due to some unresolved known issues with kwave." 15 | " See https://github.com/OpenwaterHealth/OpenLIFU-python/pull/259#issuecomment-2923230777" 16 | ) 17 | ) 18 | def test_run_simulation_runs(): 19 | """Test that run_simulation can run and outputs something of the correct type.""" 20 | 21 | transducer = openlifu.Transducer.gen_matrix_array(nx=2, ny=2, pitch=2, kerf=.5, units="mm", impulse_response=1e5) 22 | dt = 2e-7 23 | sim_setup = openlifu.SimSetup( 24 | dt=dt, 25 | t_end=3*dt, # only 3 time steps. we just want to test that the simulation code can run 26 | x_extent=(-10,10), 27 | y_extent=(-10,10), 28 | z_extent=(-2,10), 29 | ) 30 | pulse = openlifu.Pulse(frequency=400e3, duration=1/400e3) 31 | protocol = openlifu.Protocol( 32 | pulse=pulse, 33 | sequence=openlifu.Sequence(), 34 | sim_setup=sim_setup 35 | ) 36 | coords = sim_setup.get_coords() 37 | default_seg_method = protocol.seg_method 38 | params = default_seg_method.ref_params(coords) 39 | delays, apod = protocol.beamform(arr=transducer, target=openlifu.Point(position=(0,0,50)), params=params) 40 | delays[:] = 0.0 41 | apod[:] = 1.0 42 | 43 | 44 | dataset, _ = openlifu.sim.run_simulation( 45 | arr=transducer, 46 | params=params, 47 | delays=delays, 48 | apod= apod, 49 | freq = pulse.frequency, 50 | cycles = 1, 51 | dt=protocol.sim_setup.dt, 52 | t_end=protocol.sim_setup.t_end, 53 | amplitude = 1, 54 | gpu = False, 55 | ) 56 | 57 | assert isinstance(dataset, xarray.Dataset) 58 | assert 'p_max' in dataset 59 | assert 'p_min' in dataset 60 | assert 'intensity' in dataset 61 | -------------------------------------------------------------------------------- /tests/test_solution_analysis.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pytest 4 | 5 | from openlifu.plan.param_constraint import ParameterConstraint 6 | from openlifu.plan.solution_analysis import SolutionAnalysis, SolutionAnalysisOptions 7 | 8 | # ---- Tests for SolutionAnalysis ---- 9 | 10 | @pytest.fixture() 11 | def example_solution_analysis() -> SolutionAnalysis: 12 | return SolutionAnalysis( 13 | mainlobe_pnp_MPa=[1.1, 1.2], 14 | mainlobe_isppa_Wcm2=[10.0, 12.0], 15 | mainlobe_ispta_mWcm2=[500.0, 520.0], 16 | beamwidth_lat_3dB_mm=[1.5, 1.6], 17 | beamwidth_ele_3dB_mm=[2.0, 2.1], 18 | beamwidth_ax_3dB_mm=[3.0, 3.1], 19 | beamwidth_lat_6dB_mm=[1.8, 1.9], 20 | beamwidth_ele_6dB_mm=[2.5, 2.6], 21 | beamwidth_ax_6dB_mm=[3.5, 3.6], 22 | sidelobe_pnp_MPa=[0.5, 0.6], 23 | sidelobe_isppa_Wcm2=[5.0, 5.5], 24 | global_pnp_MPa=[1.3], 25 | global_isppa_Wcm2=[13.0], 26 | p0_MPa=[1.0, 1.1], 27 | TIC=0.7, 28 | power_W=25.0, 29 | MI=1.2, 30 | global_ispta_mWcm2=540.0, 31 | param_constraints={ 32 | "global_pnp_MPa": ParameterConstraint( 33 | operator="<=", 34 | warning_value=1.4, 35 | error_value=1.6 36 | ) 37 | } 38 | ) 39 | 40 | def test_to_dict_from_dict_solution_analysis(example_solution_analysis: SolutionAnalysis): 41 | sa_dict = example_solution_analysis.to_dict() 42 | new_solution = SolutionAnalysis.from_dict(sa_dict) 43 | assert new_solution == example_solution_analysis 44 | 45 | @pytest.mark.parametrize("compact", [True, False]) 46 | def test_serialize_deserialize_solution_analysis(example_solution_analysis: SolutionAnalysis, compact: bool): 47 | json_str = example_solution_analysis.to_json(compact) 48 | deserialized = SolutionAnalysis.from_json(json_str) 49 | assert deserialized == example_solution_analysis 50 | 51 | 52 | # ---- Tests for SolutionAnalysisOptions ---- 53 | 54 | 55 | @pytest.fixture() 56 | def example_solution_analysis_options() -> SolutionAnalysisOptions: 57 | return SolutionAnalysisOptions( 58 | standoff_sound_speed=1480.0, 59 | standoff_density=990.0, 60 | ref_sound_speed=1540.0, 61 | ref_density=1020.0, 62 | mainlobe_aspect_ratio=(1.0, 1.0, 4.0), 63 | mainlobe_radius=2.0e-3, 64 | beamwidth_radius=4.0e-3, 65 | sidelobe_radius=2.5e-3, 66 | sidelobe_zmin=0.5e-3, 67 | distance_units="mm", 68 | param_constraints={ 69 | "mainlobe_radius": ParameterConstraint( 70 | operator=">=", 71 | warning_value=1.5e-3, 72 | error_value=1.0e-3 73 | ) 74 | } 75 | ) 76 | 77 | def test_to_dict_from_dict_solution_analysis_options(example_solution_analysis_options: SolutionAnalysisOptions): 78 | options_dict = example_solution_analysis_options.to_dict() 79 | new_options = SolutionAnalysisOptions.from_dict(options_dict) 80 | assert new_options == example_solution_analysis_options 81 | -------------------------------------------------------------------------------- /tests/test_sonication_control_mock.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | import pytest 5 | 6 | from openlifu.bf import Pulse, Sequence 7 | from openlifu.geo import Point 8 | from openlifu.io.LIFUInterface import LIFUInterface, LIFUInterfaceStatus 9 | from openlifu.plan.solution import Solution 10 | 11 | 12 | @pytest.fixture() 13 | def example_solution() -> Solution: 14 | pt = Point(position=(0,0,30), units="mm") 15 | return Solution( 16 | id="solution", 17 | name="Solution", 18 | protocol_id="example_protocol", 19 | transducer_id="example_transducer", 20 | delays = np.zeros((1,64)), 21 | apodizations = np.ones((1,64)), 22 | pulse = Pulse(frequency=500e3, duration=2e-5), 23 | voltage=1.0, 24 | sequence = Sequence( 25 | pulse_interval=0.1, 26 | pulse_count=10, 27 | pulse_train_interval=1, 28 | pulse_train_count=1 29 | ), 30 | target=pt, 31 | foci=[pt], 32 | approved=True 33 | ) 34 | 35 | def test_lifuinterface_mock(example_solution:Solution): 36 | """Test that LIFUInterface can be used in mock mode (i.e. test_mode=True)""" 37 | lifu_interface = LIFUInterface(TX_test_mode=True, HV_test_mode=True) 38 | lifu_interface.txdevice.enum_tx7332_devices(num_devices=2) 39 | lifu_interface.set_solution(example_solution) 40 | lifu_interface.start_sonication() 41 | status = lifu_interface.get_status() 42 | assert status == LIFUInterfaceStatus.STATUS_READY 43 | lifu_interface.stop_sonication() 44 | -------------------------------------------------------------------------------- /tests/test_transducer.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | import numpy as np 6 | import pytest 7 | from helpers import dataclasses_are_equal 8 | 9 | from openlifu.xdc import Element, Transducer 10 | 11 | 12 | @pytest.fixture() 13 | def example_transducer() -> Transducer: 14 | return Transducer.from_file(Path(__file__).parent/'resources/example_db/transducers/example_transducer/example_transducer.json') 15 | 16 | @pytest.mark.parametrize("compact_representation", [True, False]) 17 | def test_serialize_deserialize_transducer(example_transducer : Transducer, compact_representation: bool): 18 | reconstructed_transducer = example_transducer.from_json(example_transducer.to_json(compact_representation)) 19 | dataclasses_are_equal(example_transducer, reconstructed_transducer) 20 | 21 | def test_get_polydata_color_options(example_transducer : Transducer): 22 | """Ensure that the color is set correctly on the polydata""" 23 | polydata_with_default_color = example_transducer.get_polydata() 24 | point_scalars = polydata_with_default_color.GetPointData().GetScalars() 25 | assert point_scalars is None 26 | 27 | polydata_with_given_color = example_transducer.get_polydata(facecolor=[0,1,1,0.5]) 28 | point_scalars = polydata_with_given_color.GetPointData().GetScalars() 29 | assert point_scalars is not None 30 | 31 | def test_default_transducer(): 32 | """Ensure it is possible to construct a default transducer""" 33 | Transducer() 34 | 35 | def test_convert_transform(): 36 | transducer = Transducer(units='cm') 37 | transform = transducer.convert_transform( 38 | matrix = np.array([ 39 | [1,0,0,2], 40 | [0,1,0,3], 41 | [0,0,1,4], 42 | [0,0,0,1], 43 | ], dtype=float), 44 | units = "m", 45 | ) 46 | expected_transform = np.array([ 47 | [1,0,0,200], 48 | [0,1,0,300], 49 | [0,0,1,400], 50 | [0,0,0,1], 51 | ], dtype=float) 52 | assert np.allclose(transform,expected_transform) 53 | 54 | def test_get_effective_origin(): 55 | transducer = Transducer.gen_matrix_array(nx=3, ny=2, units='cm') 56 | effective_origin_with_all_active = transducer.get_effective_origin(apodizations = np.ones(transducer.numelements())) 57 | assert np.allclose(effective_origin_with_all_active, np.zeros(3)) 58 | 59 | rng = np.random.default_rng() 60 | element_index_to_turn_on = rng.integers(transducer.numelements()) 61 | apodizations_with_just_one_element = np.zeros(transducer.numelements()) 62 | apodizations_with_just_one_element[element_index_to_turn_on] = 0.5 # It is allowed to be a number between 0 and 1 63 | assert np.allclose( 64 | transducer.get_effective_origin(apodizations = apodizations_with_just_one_element, units = "um"), 65 | transducer.get_positions(units="um")[element_index_to_turn_on], 66 | ) 67 | 68 | def test_get_standoff_transform_in_units(): 69 | standoff_transform_in_mm = np.array([ 70 | [-0.1,0.9,0,20], 71 | [0.9,0.1,0,30], 72 | [0,0,1,40], 73 | [0,0,0,1], 74 | ]) 75 | standoff_transform_in_cm = np.array([ 76 | [-0.1,0.9,0,2], 77 | [0.9,0.1,0,3], 78 | [0,0,1,4], 79 | [0,0,0,1], 80 | ]) 81 | transducer = Transducer(units='mm') 82 | transducer.standoff_transform = standoff_transform_in_mm 83 | assert np.allclose( 84 | transducer.get_standoff_transform_in_units("cm"), 85 | standoff_transform_in_cm, 86 | ) 87 | 88 | def test_read_data_types(example_transducer:Transducer): 89 | assert isinstance(example_transducer.standoff_transform, np.ndarray) 90 | if len(example_transducer.elements) > 0: 91 | assert isinstance(example_transducer.elements[0], Element) 92 | -------------------------------------------------------------------------------- /tests/test_units.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import numpy as np 4 | import pytest 5 | from xarray import DataArray, Dataset 6 | 7 | from openlifu.util.units import ( 8 | get_ndgrid_from_arr, 9 | getsiscale, 10 | rescale_coords, 11 | rescale_data_arr, 12 | ) 13 | 14 | 15 | @pytest.fixture() 16 | def example_xarr() -> Dataset: 17 | rng = np.random.default_rng(147) 18 | return Dataset( 19 | { 20 | 'p': DataArray( 21 | data=rng.random((3, 2)), 22 | dims=["x", "y"], 23 | attrs={'units': "Pa"} 24 | ), 25 | 'it': DataArray( 26 | data=rng.random((3, 2)), 27 | dims=["x", "y"], 28 | attrs={'units': "W/cm^2"} 29 | ) 30 | }, 31 | coords={ 32 | 'x': DataArray(dims=["x"], data=np.linspace(0, 1, 3), attrs={'units': "m"}), 33 | 'y': DataArray(dims=["y"], data=np.linspace(0, 1, 2), attrs={'units': "m"}) 34 | } 35 | ) 36 | 37 | 38 | def test_getsiscale(): 39 | with pytest.raises(ValueError, match="Unknown prefix"): 40 | getsiscale('xx','distance') 41 | 42 | assert getsiscale('mm', 'distance') == 1e-3 43 | assert getsiscale('km', 'distance') == 1e3 44 | assert getsiscale('mm^2', 'area') == 1e-6 45 | assert getsiscale('mm^3', 'volume') == 1e-9 46 | assert getsiscale('ns', 'time') == 1e-9 47 | assert getsiscale('nanosecond', 'time') == 1e-9 48 | assert getsiscale('hour', 'time') == 3600. 49 | assert getsiscale('rad', 'angle') == 1. 50 | assert np.allclose(getsiscale('deg', 'angle'), np.pi/180.) 51 | assert getsiscale('MHz', 'frequency') == 1e6 52 | assert getsiscale('GHz', 'frequency') == 1e9 53 | assert getsiscale('THz', 'frequency') == 1e12 54 | 55 | 56 | def test_rescale_data_arr(example_xarr: Dataset): 57 | """Test that an xarray data can be correctly rescaled.""" 58 | expected_p = 1e-6 * example_xarr['p'].data 59 | expected_it = 1e4 * example_xarr['it'].data 60 | rescaled_p = rescale_data_arr(example_xarr['p'], units="MPa") 61 | rescaled_it = rescale_data_arr(example_xarr['it'], units="W/m^2") 62 | 63 | np.testing.assert_almost_equal(rescaled_p, expected_p) 64 | np.testing.assert_almost_equal(rescaled_it, expected_it) 65 | 66 | 67 | def test_rescale_coords(example_xarr: Dataset): 68 | """Test that an xarray coords can be correctly rescaled.""" 69 | expected_x = 1e3 * example_xarr['p'].coords['x'].data 70 | expected_y = 1e3 * example_xarr['p'].coords['y'].data 71 | rescaled = rescale_coords(example_xarr['p'], units="mm") 72 | 73 | np.testing.assert_almost_equal(rescaled['x'].data, expected_x) 74 | np.testing.assert_almost_equal(rescaled['y'].data, expected_y) 75 | 76 | 77 | def test_get_ndgrid_from_arr(example_xarr: Dataset): 78 | """Test that an ndgrid can be constructed from the coordinates of an xarray.""" 79 | expected_X = np.array([[0., 0.], [0.5, 0.5], [1., 1.]]) 80 | expected_Y = np.array([[0., 1.], [0., 1.], [0., 1.]]) 81 | expected = np.stack([expected_X, expected_Y], axis=-1) 82 | ndgrid = get_ndgrid_from_arr(example_xarr) 83 | 84 | np.testing.assert_equal(ndgrid, expected) 85 | -------------------------------------------------------------------------------- /tests/test_user.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | import pytest 6 | 7 | from openlifu import User 8 | 9 | 10 | @pytest.fixture() 11 | def example_user() -> User: 12 | return User.from_file(Path(__file__).parent/'resources/example_db/users/example_user/example_user.json') 13 | 14 | @pytest.mark.parametrize("compact_representation", [True, False]) 15 | def test_serialize_deserialize_user(example_user : User, compact_representation: bool): 16 | assert example_user.from_json(example_user.to_json(compact_representation)) == example_user 17 | 18 | def test_default_user(): 19 | """Ensure it is possible to construct a default user""" 20 | User() 21 | -------------------------------------------------------------------------------- /tests/test_virtual_fit.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from openlifu.virtual_fit import VirtualFitOptions 4 | 5 | 6 | def test_unit_conversion(): 7 | vfo = VirtualFitOptions( 8 | units="cm", 9 | transducer_steering_center_distance=45., 10 | pitch_range=(-12,14), 11 | yaw_range=(-13,15), 12 | pitch_step = 9, 13 | yaw_step = 10, 14 | planefit_dyaw_extent = 2.3, 15 | steering_limits=((-10,11),(-12,13),(-14,15)), 16 | ) 17 | vfo_converted = vfo.to_units("mm") 18 | assert vfo_converted.transducer_steering_center_distance == 10*vfo.transducer_steering_center_distance 19 | assert vfo_converted.planefit_dyaw_extent == 10*vfo.planefit_dyaw_extent 20 | assert vfo_converted.yaw_step == vfo.yaw_step 21 | assert vfo_converted.pitch_range == vfo.pitch_range 22 | assert isinstance(vfo_converted.steering_limits, tuple) 23 | assert all(isinstance(sl, tuple) for sl in vfo_converted.steering_limits) 24 | assert vfo_converted.steering_limits[2][0] == 10*vfo.steering_limits[2][0] 25 | --------------------------------------------------------------------------------