├── docs ├── _static │ └── .gitkeep ├── includeme.rst ├── _templates │ ├── custom-class-template.rst │ └── custom-module-template.rst ├── api.rst ├── index.rst ├── Makefile ├── make.bat └── conf.py ├── src └── openlifu │ ├── py.typed │ ├── nav │ ├── __init__.py │ ├── meshroom_pipelines │ │ ├── __init__.py │ │ └── draft_pipeline.mg │ └── modnet_checkpoints │ │ └── __init__.py │ ├── _version.pyi │ ├── util │ ├── types.py │ ├── checkgpu.py │ ├── annotations.py │ ├── dict_conversion.py │ ├── json.py │ └── strings.py │ ├── io │ ├── __init__.py │ ├── LIFUSignal.py │ └── LIFUConfig.py │ ├── bf │ ├── delay_methods │ │ ├── __init__.py │ │ ├── delaymethod.py │ │ └── direct.py │ ├── focal_patterns │ │ ├── __init__.py │ │ ├── single.py │ │ ├── focal_pattern.py │ │ └── wheel.py │ ├── apod_methods │ │ ├── __init__.py │ │ ├── uniform.py │ │ ├── apodmethod.py │ │ ├── maxangle.py │ │ └── piecewiselinear.py │ ├── __init__.py │ ├── pulse.py │ └── sequence.py │ ├── seg │ ├── seg_methods │ │ ├── __init__.py │ │ └── uniform.py │ └── __init__.py │ ├── sim │ └── __init__.py │ ├── db │ ├── __init__.py │ ├── subject.py │ └── user.py │ ├── xdc │ ├── __init__.py │ └── util.py │ ├── plan │ ├── __init__.py │ ├── run.py │ └── target_constraints.py │ └── __init__.py ├── .dvc ├── .gitignore └── config ├── .gitattributes ├── tests ├── resources │ ├── example_db │ │ ├── systems │ │ │ └── systems.json │ │ ├── users │ │ │ ├── users.json │ │ │ └── example_user │ │ │ │ └── example_user.json │ │ ├── subjects │ │ │ ├── subjects.json │ │ │ └── example_subject │ │ │ │ ├── volumes │ │ │ │ ├── volumes.json │ │ │ │ └── example_volume │ │ │ │ │ ├── example_volume.json │ │ │ │ │ └── example_volume.nii │ │ │ │ ├── sessions │ │ │ │ ├── sessions.json │ │ │ │ └── example_session │ │ │ │ │ ├── runs │ │ │ │ │ ├── runs.json │ │ │ │ │ └── example_run │ │ │ │ │ │ ├── example_run.json │ │ │ │ │ │ ├── example_run_session_snapshot.json │ │ │ │ │ │ └── example_run_protocol_snapshot.json │ │ │ │ │ ├── photoscans │ │ │ │ │ ├── photoscans.json │ │ │ │ │ └── example_photoscan │ │ │ │ │ │ ├── example_photoscan_texture.exr │ │ │ │ │ │ ├── example_photoscan.json │ │ │ │ │ │ └── example_photoscan.obj │ │ │ │ │ ├── solutions │ │ │ │ │ ├── solutions.json │ │ │ │ │ └── example_solution │ │ │ │ │ │ ├── example_solution.nc │ │ │ │ │ │ ├── example_solution_analysis.json │ │ │ │ │ │ └── example_solution.json │ │ │ │ │ └── example_session.json │ │ │ │ └── example_subject.json │ │ ├── protocols │ │ │ ├── protocols.json │ │ │ └── example_protocol │ │ │ │ └── example_protocol.json │ │ └── transducers │ │ │ ├── transducers.json │ │ │ ├── example_transducer_array │ │ │ └── example_transducer_array.json │ │ │ └── example_transducer_array2 │ │ │ └── example_transducer_array2.json │ ├── CT_small.dcm │ └── dicom_series │ │ ├── 6293 │ │ └── 6924 ├── test_package.py ├── test_sequence.py ├── test_user.py ├── test_virtual_fit.py ├── test_point.py ├── test_run.py ├── helpers.py ├── test_sonication_control_mock.py ├── test_apod_methods.py ├── test_offset_grid.py ├── test_sim.py ├── test_material.py ├── test_param_constraints.py ├── test_io.py ├── test_solution_analysis.py ├── test_units.py ├── test_seg_method.py └── test_transducer.py ├── db_dvc.dvc ├── .dvcignore ├── .git_archival.txt ├── .readthedocs.yaml ├── .github ├── dependabot.yml ├── matchers │ └── pylint.json ├── workflows │ ├── cd.yml │ └── ci.yml └── CONTRIBUTING.md ├── examples ├── legacy │ ├── run_self_test.py │ ├── test_reset.py │ ├── test_toggle_12v.py │ ├── test_multiple_modules.py │ ├── test_console_dfu.py │ ├── test_temp.py │ ├── test_console2.py │ ├── test_pwr.py │ ├── test_async_mode.py │ ├── test_solution_analysis.py │ ├── test_transmitter.py │ ├── test_tx_dfu.py │ ├── test_ti_cfg.py │ ├── test_solution.py │ ├── test_simulation.py │ ├── test_updated_pwr.py │ ├── demo.py │ ├── test_nifti.py │ ├── stress_test.py │ ├── test_async.py │ └── test_nucleo.py ├── tutorials │ └── README.md ├── verification │ ├── tst02_txmodule_selftest.py │ └── tst01_console_selftest.py └── tools │ └── standardize_database.py ├── .pre-commit-config.yaml ├── .gitignore └── noxfile.py /docs/_static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/openlifu/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/openlifu/nav/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/openlifu/nav/meshroom_pipelines/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/openlifu/nav/modnet_checkpoints/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dvc/.gitignore: -------------------------------------------------------------------------------- 1 | /config.local 2 | /tmp 3 | /cache 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | .git_archival.txt export-subst 2 | -------------------------------------------------------------------------------- /tests/resources/example_db/systems/systems.json: -------------------------------------------------------------------------------- 1 | { 2 | "system_ids": [] 3 | } 4 | -------------------------------------------------------------------------------- /tests/resources/example_db/users/users.json: -------------------------------------------------------------------------------- 1 | { "user_ids": ["example_user"] } 2 | -------------------------------------------------------------------------------- /tests/resources/example_db/subjects/subjects.json: -------------------------------------------------------------------------------- 1 | { 2 | "subject_ids": ["example_subject"] 3 | } 4 | -------------------------------------------------------------------------------- /tests/resources/example_db/protocols/protocols.json: -------------------------------------------------------------------------------- 1 | { 2 | "protocol_ids": ["example_protocol"] 3 | } 4 | -------------------------------------------------------------------------------- /docs/includeme.rst: -------------------------------------------------------------------------------- 1 | Home 2 | ======== 3 | 4 | .. include:: ../README.rst 5 | :start-after: .. SPHINX-START 6 | -------------------------------------------------------------------------------- /tests/resources/CT_small.dcm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenwaterHealth/OpenLIFU-python/HEAD/tests/resources/CT_small.dcm -------------------------------------------------------------------------------- /tests/resources/example_db/subjects/example_subject/volumes/volumes.json: -------------------------------------------------------------------------------- 1 | { 2 | "volume_ids": ["example_volume"] 3 | } 4 | -------------------------------------------------------------------------------- /tests/resources/example_db/subjects/example_subject/sessions/sessions.json: -------------------------------------------------------------------------------- 1 | { 2 | "session_ids": ["example_session"] 3 | } 4 | -------------------------------------------------------------------------------- /tests/resources/dicom_series/6293: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenwaterHealth/OpenLIFU-python/HEAD/tests/resources/dicom_series/6293 -------------------------------------------------------------------------------- /tests/resources/dicom_series/6924: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenwaterHealth/OpenLIFU-python/HEAD/tests/resources/dicom_series/6924 -------------------------------------------------------------------------------- /tests/resources/example_db/subjects/example_subject/sessions/example_session/runs/runs.json: -------------------------------------------------------------------------------- 1 | { 2 | "run_ids": ["example_run"] 3 | } 4 | -------------------------------------------------------------------------------- /db_dvc.dvc: -------------------------------------------------------------------------------- 1 | outs: 2 | - md5: e128f63a052c85b2fce480a4e9f81e5e.dir 3 | nfiles: 441 4 | hash: md5 5 | path: db_dvc 6 | size: 1540218836 7 | -------------------------------------------------------------------------------- /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/solutions/solutions.json: -------------------------------------------------------------------------------- 1 | { 2 | "solution_ids": ["example_solution"] 3 | } 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/resources/example_db/subjects/example_subject/example_subject.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "example_subject", 3 | "name": "Example Subject", 4 | "attrs": {} 5 | } 6 | -------------------------------------------------------------------------------- /.git_archival.txt: -------------------------------------------------------------------------------- 1 | node: 5111929709dac2a9b5bf6bb75b933515031c4024 2 | node-date: 2025-12-08T23:21:12+08:00 3 | describe-name: v0.12.0-2-g51119297 4 | ref-names: HEAD -> main 5 | -------------------------------------------------------------------------------- /docs/_templates/custom-class-template.rst: -------------------------------------------------------------------------------- 1 | {{ fullname | escape | underline}} 2 | 3 | .. currentmodule:: {{ module }} 4 | 5 | .. autoclass:: {{ objname }} 6 | :members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /src/openlifu/util/types.py: -------------------------------------------------------------------------------- 1 | """Custom types defined for openlifu""" 2 | from __future__ import annotations 3 | 4 | import os 5 | from typing import Union 6 | 7 | PathLike = Union[str,os.PathLike] 8 | -------------------------------------------------------------------------------- /tests/resources/example_db/transducers/transducers.json: -------------------------------------------------------------------------------- 1 | { 2 | "transducer_ids": [ 3 | "example_transducer", 4 | "example_transducer_array", 5 | "example_transducer_array2" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/resources/example_db/subjects/example_subject/volumes/example_volume/example_volume.nii: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenwaterHealth/OpenLIFU-python/HEAD/tests/resources/example_db/subjects/example_subject/volumes/example_volume/example_volume.nii -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /tests/resources/example_db/subjects/example_subject/sessions/example_session/solutions/example_solution/example_solution.nc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenwaterHealth/OpenLIFU-python/HEAD/tests/resources/example_db/subjects/example_subject/sessions/example_session/solutions/example_solution/example_solution.nc -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /tests/resources/example_db/subjects/example_subject/sessions/example_session/photoscans/example_photoscan/example_photoscan_texture.exr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenwaterHealth/OpenLIFU-python/HEAD/tests/resources/example_db/subjects/example_subject/sessions/example_session/photoscans/example_photoscan/example_photoscan_texture.exr -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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/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/xdc/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from .element import Element 4 | from .transducer import Transducer, TransformedTransducer 5 | from .transducerarray import TransducerArray, get_angle_from_gap, get_roc_from_angle 6 | 7 | __all__ = [ 8 | "element", 9 | "transducer", 10 | "Element", 11 | "Transducer", 12 | "TransformedTransducer", 13 | "TransducerArray", 14 | "get_angle_from_gap", 15 | "get_roc_from_angle" 16 | ] 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /examples/legacy/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/legacy/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 | -------------------------------------------------------------------------------- /examples/legacy/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/tutorials/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 | -------------------------------------------------------------------------------- /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 | ] = ("x", "y", "z") 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 pandas as pd 8 | import xarray as xa 9 | 10 | from openlifu.bf import delay_methods 11 | from openlifu.geo import Point 12 | from openlifu.xdc import Transducer 13 | 14 | 15 | @dataclass 16 | class DelayMethod(ABC): 17 | @abstractmethod 18 | def calc_delays(self, arr: Transducer, target: Point, params: xa.Dataset, transform:np.ndarray | None=None): 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 = delay_methods.__dict__ 31 | class_constructor = module_dict[short_classname] 32 | return class_constructor(**d) 33 | 34 | @abstractmethod 35 | def to_table(self) -> pd.DataFrame: 36 | """ 37 | Get a table of the delay method parameters 38 | 39 | :returns: Pandas DataFrame of the delay method parameters 40 | """ 41 | pass 42 | -------------------------------------------------------------------------------- /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 pandas as pd 8 | import xarray as xa 9 | 10 | from openlifu.bf.apod_methods import ApodizationMethod 11 | from openlifu.geo import Point 12 | from openlifu.util.annotations import OpenLIFUFieldData 13 | from openlifu.xdc import Transducer 14 | 15 | 16 | @dataclass 17 | class Uniform(ApodizationMethod): 18 | value: Annotated[float, OpenLIFUFieldData("Value", "Uniform apodization value between 0 and 1.")] = 1.0 19 | """Uniform apodization value between 0 and 1.""" 20 | 21 | def calc_apodization(self, arr: Transducer, target: Point, params: xa.Dataset, transform:np.ndarray | None=None): 22 | return np.full(arr.numelements(), self.value) 23 | 24 | def to_table(self): 25 | """ 26 | Get a table of the apodization method parameters 27 | 28 | :returns: Pandas DataFrame of the apodization method parameters 29 | """ 30 | records = [{"Name": "Type", "Value": "Uniform", "Unit": ""}, 31 | {"Name": "Value", "Value": self.value, "Unit": ""}] 32 | return pd.DataFrame.from_records(records) 33 | -------------------------------------------------------------------------------- /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/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 pandas as pd 9 | import xarray as xa 10 | 11 | from openlifu.bf import apod_methods 12 | from openlifu.geo import Point 13 | from openlifu.xdc import Transducer 14 | 15 | 16 | @dataclass 17 | class ApodizationMethod(ABC): 18 | @abstractmethod 19 | def calc_apodization(self, arr: Transducer, target: Point, params: xa.Dataset, transform:np.ndarray | None=None) -> Any: 20 | pass 21 | 22 | def to_dict(self): 23 | d = self.__dict__.copy() 24 | d['class'] = self.__class__.__name__ 25 | return d 26 | 27 | @staticmethod 28 | def from_dict(d): 29 | d = d.copy() 30 | short_classname = d.pop("class") 31 | module_dict = apod_methods.__dict__ 32 | class_constructor = module_dict[short_classname] 33 | return class_constructor(**d) 34 | 35 | @abstractmethod 36 | def to_table(self) -> pd.DataFrame: 37 | """ 38 | Get a table of the apodization method parameters 39 | 40 | :returns: Pandas DataFrame of the apodization method parameters 41 | """ 42 | pass 43 | -------------------------------------------------------------------------------- /.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@v6 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@v6 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/openlifu/bf/focal_patterns/single.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | 5 | import pandas as pd 6 | 7 | from openlifu.bf.focal_patterns import FocalPattern 8 | from openlifu.geo import Point 9 | 10 | 11 | @dataclass 12 | class SinglePoint(FocalPattern): 13 | """ 14 | Class for representing a single focus 15 | 16 | :ivar target_pressure: Target pressure of the focal pattern in Pa 17 | """ 18 | def get_targets(self, target: Point): 19 | """ 20 | Get the targets of the focal pattern 21 | 22 | :param target: Target point of the focal pattern 23 | :returns: List of target points 24 | """ 25 | return [target.copy()] 26 | 27 | def num_foci(self): 28 | """ 29 | Get the number of foci in the focal pattern 30 | 31 | :returns: Number of foci (1) 32 | """ 33 | return 1 34 | 35 | def to_table(self) -> pd.DataFrame: 36 | """ 37 | Get a table of the focal pattern parameters 38 | 39 | :returns: Pandas DataFrame of the focal pattern parameters 40 | """ 41 | records = [{"Name": "Type", "Value": "Single Point", "Unit": ""}, 42 | {"Name": "Target Pressure", "Value": self.target_pressure, "Unit": self.units}] 43 | return pd.DataFrame.from_records(records) 44 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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=None, 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 | -------------------------------------------------------------------------------- /src/openlifu/xdc/util.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | 5 | from openlifu.util.types import PathLike 6 | from openlifu.xdc.transducer import Transducer 7 | from openlifu.xdc.transducerarray import TransducerArray 8 | 9 | 10 | def load_transducer_from_file(transducer_filepath : PathLike, convert_array:bool = True) -> Transducer|TransducerArray: 11 | """Load a Transducer or TransducerArray from file, depending on the "type" field in the file. 12 | Note: the transducer object includes the relative path to the affiliated transducer model data. `get_transducer_absolute_filepaths`, should 13 | be used to obtain the absolute data filepaths based on the Database directory path. 14 | Args: 15 | transducer_filepath: path to the transducer json file 16 | convert_array: When enabled, if a TransducerArray is encountered then it is converted to a Transducer. 17 | Returns: a Transducer if the json file defines a Transducer, or if the json file defines a TransducerArray and convert_array is enabled. 18 | Otherwise a TransducerArray. 19 | """ 20 | with open(transducer_filepath) as f: 21 | if not f: 22 | raise FileNotFoundError(f"Transducer file not found at: {transducer_filepath}") 23 | d = json.load(f) 24 | if "type" in d and d["type"] == "TransducerArray": 25 | transducer = TransducerArray.from_dict(d) 26 | if convert_array: 27 | transducer = transducer.to_transducer() 28 | else: 29 | transducer = Transducer.from_file(transducer_filepath) 30 | return transducer 31 | -------------------------------------------------------------------------------- /examples/legacy/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.start_sonication(): 35 | print("Trigger Running Press enter to STOP:") 36 | input() # Wait for the user to press Enter 37 | if interface.stop_sonication(): 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 | -------------------------------------------------------------------------------- /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/__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, TransducerArray 39 | 40 | from ._version import version as __version__ 41 | 42 | __all__ = [ 43 | "Point", 44 | "Transducer", 45 | "TransducerArray", 46 | "Protocol", 47 | "Solution", 48 | "Material", 49 | "SegmentationMethod", 50 | "seg_methods", 51 | "MATERIALS", 52 | "WATER", 53 | "TISSUE", 54 | "SKULL", 55 | "AIR", 56 | "STANDOFF", 57 | "DelayMethod", 58 | "ApodizationMethod", 59 | "Pulse", 60 | "Sequence", 61 | "FocalPattern", 62 | "focal_patterns", 63 | "delay_methods", 64 | "apod_methods", 65 | "SimSetup", 66 | "Database", 67 | "User", 68 | "VirtualFitOptions", 69 | "run_virtual_fit", 70 | "LIFUInterface", 71 | "__version__", 72 | ] 73 | -------------------------------------------------------------------------------- /examples/legacy/test_console_dfu.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from time import sleep 5 | 6 | from openlifu.io.LIFUInterface import LIFUInterface 7 | 8 | # set PYTHONPATH=%cd%\src;%PYTHONPATH% 9 | # python notebooks/test_console_dfu.py 10 | """ 11 | Test script to automate: 12 | 1. Connect to the device. 13 | 2. Test HVController: Turn HV on/off and check voltage. 14 | 3. Test Device functionality. 15 | """ 16 | print("Starting LIFU Test Script...") 17 | interface = LIFUInterface(TX_test_mode=False) 18 | tx_connected, hv_connected = interface.is_device_connected() 19 | if tx_connected and hv_connected: 20 | print("LIFU Device Fully connected.") 21 | else: 22 | print(f'LIFU Device NOT Fully Connected. TX: {tx_connected}, HV: {hv_connected}') 23 | 24 | if not hv_connected: 25 | print("HV Controller not connected.") 26 | sys.exit(1) 27 | 28 | print("Ping the device") 29 | interface.hvcontroller.ping() 30 | 31 | # Ask the user for confirmation 32 | user_input = input("Do you want to Enter DFU Mode? (y/n): ").strip().lower() 33 | 34 | if user_input == 'y': 35 | print("Enter DFU mode") 36 | if interface.hvcontroller.enter_dfu(): 37 | print("Successful.") 38 | 39 | print("Use stm32 cube programmer to update firmware, power cycle will put the console back into an operating state") 40 | sys.exit(0) 41 | 42 | elif user_input == 'n': 43 | print("Reset device") 44 | if interface.hvcontroller.soft_reset(): 45 | print("Successful.") 46 | 47 | sleep(6) 48 | interface.hvcontroller.uart.reopen_after_reset() 49 | print("Ping the device again") 50 | if interface.hvcontroller.ping(): 51 | print("Test script complete.") 52 | else: 53 | print("Device did not respond after reset.") 54 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/legacy/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(' pd.DataFrame: 41 | """ 42 | Get a table of the delay method parameters 43 | 44 | :returns: Pandas DataFrame of the delay method parameters 45 | """ 46 | records = [{"Name": "Type", "Value": "Direct", "Unit": ""}, 47 | {"Name": "Default Sound Speed", "Value": self.c0, "Unit": "m/s"}] 48 | return pd.DataFrame.from_records(records) 49 | -------------------------------------------------------------------------------- /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", sensitivity=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/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 | -------------------------------------------------------------------------------- /examples/legacy/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 | -------------------------------------------------------------------------------- /.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@v6 25 | with: 26 | fetch-depth: 0 27 | - uses: actions/setup-python@v6 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@v6 50 | with: 51 | fetch-depth: 0 52 | 53 | - uses: actions/setup-python@v6 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.5.1 72 | with: 73 | token: ${{ secrets.CODECOV_TOKEN }} 74 | -------------------------------------------------------------------------------- /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_ASYNC = 0x09 36 | OW_CMD_DFU = 0x0D 37 | OW_CMD_NOP = 0x0E 38 | OW_CMD_RESET = 0x0F 39 | 40 | # Controller Commands 41 | OW_CTRL_SET_SWTRIG = 0x13 42 | OW_CTRL_GET_SWTRIG = 0x14 43 | OW_CTRL_START_SWTRIG = 0x15 44 | OW_CTRL_STOP_SWTRIG = 0x16 45 | OW_CTRL_STATUS_SWTRIG = 0x17 46 | OW_CTRL_RESET = 0x1F 47 | 48 | # TX7332 Commands 49 | OW_TX7332_STATUS = 0x20 50 | OW_TX7332_ENUM = 0x21 51 | OW_TX7332_WREG = 0x22 52 | OW_TX7332_RREG = 0x23 53 | OW_TX7332_WBLOCK = 0x24 54 | OW_TX7332_VWREG = 0x25 55 | OW_TX7332_VWBLOCK = 0x26 56 | OW_TX7332_DEVICE_COUNT = 0x2C 57 | OW_TX7332_DEMO = 0x2D 58 | OW_TX7332_RESET = 0x2F 59 | 60 | # Power Commands 61 | OW_POWER_STATUS = 0x30 62 | OW_POWER_SET_HV = 0x31 63 | OW_POWER_GET_HV = 0x32 64 | OW_POWER_HV_ON = 0x33 65 | OW_POWER_HV_OFF = 0x34 66 | OW_POWER_12V_ON = 0x35 67 | OW_POWER_12V_OFF = 0x36 68 | OW_POWER_GET_TEMP1 = 0x37 69 | OW_POWER_GET_TEMP2 = 0x38 70 | OW_POWER_SET_FAN = 0x39 71 | OW_POWER_GET_FAN = 0x3A 72 | OW_POWER_SET_RGB = 0x3B 73 | OW_POWER_GET_RGB = 0x3C 74 | OW_POWER_GET_HVON = 0x3D 75 | OW_POWER_GET_12VON = 0x3E 76 | OW_POWER_SET_DACS = 0x3F 77 | OW_POWER_VMON = 0x40 78 | OW_POWER_RAW_DAC = 0x41 79 | OW_POWER_HV_ENABLE = 0x42 80 | 81 | TRIGGER_MODE_SEQUENCE = 0 82 | TRIGGER_MODE_CONTINUOUS = 1 83 | TRIGGER_MODE_SINGLE = 2 84 | 85 | TRIGGER_STATUS_READY = 0 86 | TRIGGER_STATUS_RUNNING = 1 87 | TRIGGER_STATUS_ERROR = 2 88 | TRIGGER_STATUS_NOT_CONFIGURED = 3 89 | -------------------------------------------------------------------------------- /examples/legacy/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 | -------------------------------------------------------------------------------- /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 pandas as pd 8 | import xarray as xa 9 | 10 | from openlifu.bf.apod_methods import ApodizationMethod 11 | from openlifu.geo import Point 12 | from openlifu.util.annotations import OpenLIFUFieldData 13 | from openlifu.util.units import getunittype 14 | from openlifu.xdc import Transducer 15 | 16 | 17 | @dataclass 18 | class MaxAngle(ApodizationMethod): 19 | max_angle: Annotated[float, OpenLIFUFieldData("Maximum acceptance angle", "Maximum acceptance angle for each element from the vector normal to the element surface")] = 30.0 20 | """Maximum acceptance angle for each element from the vector normal to the element surface""" 21 | 22 | units: Annotated[str, OpenLIFUFieldData("Angle units", "Angle units")] = "deg" 23 | """Angle units""" 24 | 25 | def __post_init__(self): 26 | if not isinstance(self.max_angle, (int, float)): 27 | raise TypeError(f"Max angle must be a number, got {type(self.max_angle).__name__}.") 28 | if self.max_angle < 0: 29 | raise ValueError(f"Max angle must be non-negative, got {self.max_angle}.") 30 | if getunittype(self.units) != "angle": 31 | raise ValueError(f"Units must be an angle type, got {self.units}.") 32 | 33 | def calc_apodization(self, arr: Transducer, target: Point, params: xa.Dataset, transform:np.ndarray | None=None): 34 | target_pos = target.get_position(units="m") 35 | matrix = transform if transform is not None else np.eye(4) 36 | angles = np.array([el.angle_to_point(target_pos, units="m", matrix=matrix, return_as=self.units) for el in arr.elements]) 37 | apod = np.zeros(arr.numelements()) 38 | apod[angles <= self.max_angle] = 1 39 | return apod 40 | 41 | def to_table(self) -> pd.DataFrame: 42 | """ 43 | Get a table of the apodization method parameters 44 | 45 | :returns: Pandas DataFrame of the apodization method parameters 46 | """ 47 | records = [{"Name": "Type", "Value": "Max Angle", "Unit": ""}, 48 | {"Name": "Max Angle", "Value": self.max_angle, "Unit": self.units}] 49 | return pd.DataFrame.from_records(records) 50 | -------------------------------------------------------------------------------- /examples/legacy/test_async_mode.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_async_mode.py 10 | 11 | def main(): 12 | print("Starting LIFU Async Test Script...") 13 | interface = LIFUInterface() 14 | tx_connected, hv_connected = interface.is_device_connected() 15 | 16 | if not tx_connected and not hv_connected: 17 | print("✅ LIFU Console not connected.") 18 | sys.exit(1) 19 | 20 | if not tx_connected: 21 | print("TX device not connected. Attempting to turn on 12V...") 22 | interface.hvcontroller.turn_12v_on() 23 | time.sleep(2) 24 | 25 | interface.stop_monitoring() 26 | del interface 27 | time.sleep(3) 28 | 29 | print("Reinitializing LIFU interface after powering 12V...") 30 | interface = LIFUInterface() 31 | tx_connected, hv_connected = interface.is_device_connected() 32 | 33 | if tx_connected and hv_connected: 34 | print("✅ LIFU Device fully connected.") 35 | else: 36 | print("❌ LIFU Device NOT fully connected.") 37 | print(f" TX Connected: {tx_connected}") 38 | print(f" HV Connected: {hv_connected}") 39 | sys.exit(1) 40 | 41 | print("Ping the device") 42 | if not interface.txdevice.ping(): 43 | print("❌ Failed comms with txdevice.") 44 | sys.exit(1) 45 | 46 | version = interface.txdevice.get_version() 47 | print(f"Version: {version}") 48 | 49 | curr_mode = interface.txdevice.async_mode() 50 | print(f"Current Async Mode: {curr_mode}") 51 | time.sleep(1) 52 | if curr_mode: 53 | print("Async mode is already enabled.") 54 | else: 55 | print("Enabling Async Mode...") 56 | interface.txdevice.async_mode(True) 57 | time.sleep(1) 58 | curr_mode = interface.txdevice.async_mode() 59 | print(f"Async Mode Enabled: {curr_mode}") 60 | time.sleep(1) 61 | print("Disabling Async Mode...") 62 | interface.txdevice.async_mode(False) 63 | time.sleep(1) 64 | curr_mode = interface.txdevice.async_mode() 65 | print(f"Async Mode Enabled: {curr_mode}") 66 | 67 | if __name__ == "__main__": 68 | main() 69 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | "spacing": 1, 76 | "units": "mm", 77 | "x_extent": [-30, 30], 78 | "y_extent": [-30, 30], 79 | "z_extent": [-4, 70], 80 | "dt": 0, 81 | "t_end": 0, 82 | "options": {} 83 | }, 84 | "param_constraints": {}, 85 | "target_constraints": {}, 86 | "analysis_options": {} 87 | } 88 | -------------------------------------------------------------------------------- /examples/legacy/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", sensitivity=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 | -------------------------------------------------------------------------------- /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 to_table(self) -> pd.DataFrame: 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 | -------------------------------------------------------------------------------- /examples/legacy/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.start_sonication(): 65 | print("Trigger Running Press enter to STOP:") 66 | input() # Wait for the user to press Enter 67 | if interface.stop_sonication(): 68 | print("Trigger stopped successfully.") 69 | else: 70 | print("Failed to stop trigger.") 71 | else: 72 | print("Failed to get trigger setting.") 73 | -------------------------------------------------------------------------------- /examples/legacy/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 | 14 | tx_connected, hv_connected = interface.is_device_connected() 15 | 16 | if not tx_connected and not hv_connected: 17 | print("✅ LIFU Console not connected.") 18 | sys.exit(1) 19 | 20 | if not tx_connected: 21 | print("TX device not connected. Attempting to turn on 12V...") 22 | interface.hvcontroller.turn_12v_on() 23 | 24 | # Give time for the TX device to power up and enumerate over USB 25 | time.sleep(2) 26 | 27 | # Cleanup and recreate interface to reinitialize USB devices 28 | interface.stop_monitoring() 29 | del interface 30 | time.sleep(5) # Short delay before recreating 31 | 32 | print("Reinitializing LIFU interface after powering 12V...") 33 | interface = LIFUInterface() 34 | 35 | # Re-check connection 36 | tx_connected, hv_connected = interface.is_device_connected() 37 | 38 | if tx_connected: 39 | print("✅ LIFU Device TX connected.") 40 | else: 41 | print("❌ LIFU Device NOT fully connected.") 42 | print(f" TX Connected: {tx_connected}") 43 | print(f" HV Connected: {hv_connected}") 44 | sys.exit(1) 45 | 46 | 47 | print("Ping the device") 48 | if not interface.txdevice.ping(): 49 | print("❌ failed to communicate with transmit module") 50 | sys.exit(1) 51 | 52 | print("Get Version") 53 | version = interface.txdevice.get_version() 54 | print(f"Version: {version}") 55 | 56 | 57 | # Ask the user for confirmation 58 | user_input = input("Do you want to Enter DFU Mode? (y/n): ").strip().lower() 59 | 60 | if user_input == 'y': 61 | print("Enter DFU mode") 62 | if interface.txdevice.enter_dfu(): 63 | print("Successful.") 64 | 65 | print("Use stm32 cube programmer to update firmware, power cycle will put the console back into an operating state") 66 | sys.exit(0) 67 | 68 | elif user_input == 'n': 69 | print("Reset device") 70 | if interface.txdevice.soft_reset(): 71 | print("Successful.") 72 | 73 | time.sleep(6) 74 | interface.txdevice.uart.reopen_after_reset() 75 | print("Ping the device again") 76 | if interface.txdevice.ping(): 77 | print("Test script complete.") 78 | else: 79 | print("Device did not respond after reset.") 80 | -------------------------------------------------------------------------------- /src/openlifu/seg/seg_methods/uniform.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import pandas as pd 4 | import xarray as xa 5 | 6 | from openlifu.seg.material import MATERIALS, Material 7 | from openlifu.seg.seg_method import SegmentationMethod 8 | 9 | 10 | class UniformSegmentation(SegmentationMethod): 11 | def _segment(self, volume: xa.DataArray): 12 | return self._ref_segment(volume.coords) 13 | 14 | def to_table(self) -> pd.DataFrame: 15 | """ 16 | Get a table of the segmentation method parameters 17 | 18 | :returns: Pandas DataFrame of the segmentation method parameters 19 | """ 20 | records = [{"Name": "Type", "Value": "Uniform", "Unit": ""}, 21 | {"Name": "Reference Material", "Value": self.ref_material, "Unit": ""}] 22 | return pd.DataFrame.from_records(records) 23 | 24 | class UniformTissue(UniformSegmentation): 25 | """ Assigns the tissue material to all voxels in the volume. """ 26 | def __init__(self, materials: dict[str, Material] | None = None): 27 | if materials is None: 28 | materials = MATERIALS.copy() 29 | super().__init__(materials=materials, ref_material="tissue") 30 | 31 | def to_table(self) -> pd.DataFrame: 32 | """ 33 | Get a table of the segmentation method parameters 34 | 35 | :returns: Pandas DataFrame of the segmentation method parameters 36 | """ 37 | records = [{"Name": "Type", "Value": "Uniform Tissue", "Unit": ""}] 38 | return pd.DataFrame.from_records(records) 39 | 40 | def to_dict(self): 41 | d = super().to_dict() 42 | d.pop("ref_material") 43 | return d 44 | 45 | 46 | class UniformWater(UniformSegmentation): 47 | """ Assigns the water material to all voxels in the volume. """ 48 | def __init__(self, materials: dict[str, Material] | None = None): 49 | if materials is None: 50 | materials = MATERIALS.copy() 51 | super().__init__(materials=materials, ref_material="water") 52 | 53 | def to_table(self) -> pd.DataFrame: 54 | """ 55 | Get a table of the segmentation method parameters 56 | 57 | :returns: Pandas DataFrame of the segmentation method parameters 58 | """ 59 | records = [{"Name": "Type", "Value": "Uniform Water", "Unit": ""}] 60 | return pd.DataFrame.from_records(records) 61 | 62 | def to_dict(self): 63 | d = super().to_dict() 64 | d.pop("ref_material") 65 | return d 66 | -------------------------------------------------------------------------------- /examples/legacy/test_ti_cfg.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_ti_cfg.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 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.start_sonication(): 67 | print("Trigger Running Press enter to STOP:") 68 | input() # Wait for the user to press Enter 69 | if interface.stop_sonication(): 70 | print("Trigger stopped successfully.") 71 | else: 72 | print("Failed to stop trigger.") 73 | else: 74 | print("Failed to get trigger setting.") 75 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/legacy/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 | example_transducer = openlifu.Transducer.gen_matrix_array(nx=8, ny=8, pitch=4, kerf=0.5, id="example_transducer") 26 | sequence = openlifu.Sequence( 27 | pulse_interval=0.1, 28 | pulse_count=10, 29 | pulse_train_interval=1, 30 | pulse_train_count=1 31 | ) 32 | solution = openlifu.Solution( 33 | id="solution", 34 | name="Solution", 35 | protocol_id="example_protocol", 36 | transducer=example_transducer, 37 | delays = np.zeros((1,64)), 38 | apodizations = np.ones((1,64)), 39 | pulse = pulse, 40 | voltage=1.0, 41 | sequence = sequence, 42 | target=pt, 43 | foci=[pt], 44 | approved=True 45 | ) 46 | 47 | 48 | solution 49 | 50 | ifx = openlifu.LIFUInterface() 51 | 52 | ifx.set_solution(solution.to_dict()) 53 | 54 | txm = ifx.txdevice.tx_registers 55 | r = {'DELAY CONTROL': {}, 'DELAY DATA': {}, 'PULSE CONTROL': {}, 'PULSE DATA': {}} 56 | rtemplate = {} 57 | profiles = ifx.txdevice.tx_registers.configured_pulse_profiles() 58 | for index in profiles: 59 | rtemplate[index] = ['---------']*2 60 | for index in profiles: 61 | txm.activate_delay_profile(index) 62 | txm.activate_pulse_profile(index) 63 | rdcm = txm.get_delay_control_registers() 64 | rddm = txm.get_delay_data_registers() 65 | rpcm = txm.get_pulse_control_registers() 66 | rpdm = txm.get_pulse_data_registers() 67 | d = {'DELAY CONTROL': rdcm, 'DELAY DATA': rddm, 'PULSE CONTROL': rpcm, 'PULSE DATA': rpdm} 68 | for k, rm in d.items(): 69 | for txi, rc in enumerate(rm): 70 | for addr, value in rc.items(): 71 | if addr not in r[k]: 72 | r[k][addr] = {i: ['---------']*2 for i in profiles} 73 | r[k][addr][index][txi] = f'x{value:08x}' 74 | h = [f'{"Profile " + str(i):19s}' for i in profiles] 75 | print(f" {' | '.join(h)}") 76 | h1 = [f'{"TX" + str(i):>9s}' for i in [0,1]] 77 | h1s = [' '.join(h1)]*len(profiles) 78 | print(f"addr: {' | '.join(h1s)}") 79 | for k, rm in r.items(): 80 | print(f"{k}") 81 | for addr, rr in rm.items(): 82 | print(f"x{addr:03x}: {' | '.join([' '.join(rr[i]) for i in profiles])}") 83 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/legacy/test_simulation.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import os 4 | from pathlib import Path 5 | 6 | if os.name == 'nt': 7 | pass 8 | else: 9 | pass 10 | 11 | import openlifu 12 | from openlifu.bf import apod_methods, focal_patterns 13 | from openlifu.bf.pulse import Pulse 14 | from openlifu.bf.sequence import Sequence 15 | from openlifu.db import Database 16 | from openlifu.geo import Point 17 | from openlifu.plan import Protocol 18 | from openlifu.sim import SimSetup 19 | 20 | xInput = 0 21 | yInput = 0 22 | zInput = 30 23 | 24 | frequency_kHz = 150 # Frequency in kHz 25 | duration_msec = 100 # Pulse Duration in milliseconds 26 | interval_msec = 200 # Pulse Repetition Interval in milliseconds 27 | num_modules = 1 # Number of modules in the system 28 | 29 | use_external_power_supply = False # Select whether to use console or power supply 30 | 31 | console_shutoff_temp_C = 70.0 # Console shutoff temperature in Celsius 32 | tx_shutoff_temp_C = 70.0 # TX device shutoff temperature in Celsius 33 | ambient_shutoff_temp_C = 70.0 # Ambient shutoff temperature in Celsius 34 | 35 | #TODO: script_timeout_minutes = 30 # Prevent unintentionally leaving unit on for too long 36 | #TODO: log_temp_to_csv_file = True # Log readings to only terminal or both terminal and CSV file 37 | 38 | # Fail-safe parameters if the temperature jumps too fast 39 | rapid_temp_shutoff_C = 40 # Cutoff temperature in Celsius if it jumps too fast 40 | rapid_temp_shutoff_seconds = 5 # Time in seconds to reach rapid temperature shutoff 41 | rapid_temp_increase_per_second_shutoff_C = 3 # Rapid temperature climbing shutoff in Celsius 42 | db_path = Path(openlifu.__file__).parent.parent.parent / "db_dvc" 43 | db = Database(db_path) 44 | arr = db.load_transducer(f"openlifu_{num_modules}x400_evt1") 45 | 46 | arr.sort_by_pin() 47 | 48 | target = Point(position=(xInput,yInput,zInput), units="mm") 49 | 50 | pulse = Pulse(frequency=frequency_kHz*1e3, duration=duration_msec*1e-3) 51 | sequence = Sequence( 52 | pulse_interval=interval_msec*1e-3, 53 | pulse_count=int(60/(interval_msec*1e-3)), 54 | pulse_train_interval=0, 55 | pulse_train_count=1) 56 | focal_pattern = focal_patterns.SinglePoint(target_pressure=300e3) 57 | apod_method = apod_methods.Uniform() 58 | sim_setup = SimSetup(x_extent=(-55,55), y_extent=(-30,30), z_extent=(-4,70)) 59 | protocol = Protocol( 60 | id='test_protocol', 61 | name='Test Protocol', 62 | pulse=pulse, 63 | sequence=sequence, 64 | focal_pattern=focal_pattern, 65 | apod_method=apod_method, 66 | sim_setup=sim_setup) 67 | 68 | solution, sim_res, scaled_analysis = protocol.calc_solution( 69 | target=target, 70 | transducer=arr, 71 | voltage=65, 72 | simulate=True, 73 | scale=False) 74 | 75 | print(solution.analyze().to_table()) 76 | -------------------------------------------------------------------------------- /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 | import pandas as pd 8 | 9 | from openlifu.bf import focal_patterns 10 | from openlifu.geo import Point 11 | from openlifu.util.annotations import OpenLIFUFieldData 12 | from openlifu.util.units import getunittype 13 | 14 | 15 | @dataclass 16 | class FocalPattern(ABC): 17 | """ 18 | Abstract base class for representing a focal pattern 19 | """ 20 | 21 | target_pressure: Annotated[float, OpenLIFUFieldData("Target pressure", "Target pressure of the focal pattern in given units")] = 1.0 22 | """Target pressure of the focal pattern in given units""" 23 | 24 | units: Annotated[str, OpenLIFUFieldData("Pressure units", "Pressure units")] = "Pa" 25 | """Pressure units""" 26 | 27 | def __post_init__(self): 28 | if self.target_pressure <= 0: 29 | raise ValueError("Target pressure must be greater than 0") 30 | if not isinstance(self.units, str): 31 | raise TypeError("Units must be a string") 32 | if getunittype(self.units) != 'pressure': 33 | raise ValueError(f"Units must be a pressure unit, got {self.units}") 34 | 35 | @abstractmethod 36 | def get_targets(self, target: Point): 37 | """ 38 | Get the targets of the focal pattern 39 | 40 | :param target: Target point of the focal pattern 41 | :returns: List of target points 42 | """ 43 | pass 44 | 45 | @abstractmethod 46 | def num_foci(self): 47 | """ 48 | Get the number of foci in the focal pattern 49 | 50 | :returns: Number of foci 51 | """ 52 | pass 53 | 54 | def to_dict(self): 55 | """ 56 | Convert the focal pattern to a dictionary 57 | 58 | :returns: Dictionary of the focal pattern parameters 59 | """ 60 | d = self.__dict__.copy() 61 | d['class'] = self.__class__.__name__ 62 | return d 63 | 64 | @staticmethod 65 | def from_dict(d): 66 | """ 67 | Create a focal pattern from a dictionary 68 | 69 | :param d: Dictionary of the focal pattern parameters 70 | :returns: FocalPattern object 71 | """ 72 | d = d.copy() 73 | short_classname = d.pop("class") 74 | module_dict = focal_patterns.__dict__ 75 | class_constructor = module_dict[short_classname] 76 | return class_constructor(**d) 77 | 78 | @abstractmethod 79 | def to_table(self) -> pd.DataFrame: 80 | """ 81 | Get a table of the focal pattern parameters 82 | 83 | :returns: Pandas DataFrame of the focal pattern parameters 84 | """ 85 | pass 86 | -------------------------------------------------------------------------------- /examples/legacy/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 | -------------------------------------------------------------------------------- /.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 | 92 | exclude: (?x)( ^examples ) 93 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/resources/example_db/transducers/example_transducer_array/example_transducer_array.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "TransducerArray", 3 | "id": "example_transducer_array", 4 | "name": "test example of an *array* of *transformed* transducers", 5 | "modules": [ 6 | { 7 | "id": "module1", 8 | "name": "Module 1", 9 | "elements": [ 10 | { 11 | "index": 1, 12 | "position": [-17.5, 17.5, 0.0], 13 | "orientation": [0.0, 0.0, 0.0], 14 | "size": [3.7, 3.7], 15 | "pin": 1, 16 | "units": "mm" 17 | }, 18 | { 19 | "index": 2, 20 | "position": [-17.5, 13.5, 0.0], 21 | "orientation": [0.0, 0.0, 0.0], 22 | "size": [3.7, 3.7], 23 | "pin": 2, 24 | "units": "mm" 25 | } 26 | ], 27 | "frequency": 175000.0, 28 | "units": "mm", 29 | "attrs": {}, 30 | "registration_surface_filename": null, 31 | "transducer_body_filename": null, 32 | "standoff_transform": [ 33 | [1.0, 0.0, 0.0, 0.0], 34 | [0.0, 1.0, 0.0, 0.0], 35 | [0.0, 0.0, 1.0, 0.0], 36 | [0.0, 0.0, 0.0, 1.0] 37 | ], 38 | "impulse_response": [1000.0, 1000.0], 39 | "impulse_dt": 1, 40 | "sensitivity": 1200, 41 | "module_invert": [false], 42 | "transform": [ 43 | [0.9, 0.0, -0.2, 22.8], 44 | [0.0, 1.0, 0.0, 0.0], 45 | [0.2, 0.0, 0.9, 3.3], 46 | [0.0, 0.0, 0.0, 1.0] 47 | ] 48 | }, 49 | { 50 | "id": "module2", 51 | "name": "Module 2", 52 | "elements": [ 53 | { 54 | "index": 1, 55 | "position": [-17.5, 17.5, 0.0], 56 | "orientation": [0.0, 0.0, 0.0], 57 | "size": [3.7, 3.7], 58 | "pin": 1, 59 | "units": "mm" 60 | }, 61 | { 62 | "index": 2, 63 | "position": [-17.5, 13.5, 0.0], 64 | "orientation": [0.0, 0.0, 0.0], 65 | "size": [3.7, 3.7], 66 | "pin": 2, 67 | "units": "mm" 68 | } 69 | ], 70 | "frequency": 180000.0, 71 | "units": "mm", 72 | "attrs": {}, 73 | "registration_surface_filename": null, 74 | "transducer_body_filename": null, 75 | "standoff_transform": [ 76 | [1.0, 0.0, 0.0, 0.0], 77 | [0.0, 1.0, 0.0, 0.0], 78 | [0.0, 0.0, 1.0, 0.0], 79 | [0.0, 0.0, 0.0, 1.0] 80 | ], 81 | "impulse_response": [1000.0, 1000.0], 82 | "impulse_dt": 1, 83 | "sensitivity": 1000, 84 | "module_invert": [false], 85 | "transform": [ 86 | [0.9, 0.0, 0.2, -22.0], 87 | [0.0, 1.0, 0.0, 0.0], 88 | [-0.2, 0.0, 0.9, 4.2], 89 | [0.0, 0.0, 0.0, 1.0] 90 | ] 91 | } 92 | ], 93 | "attrs": { 94 | "standoff_transform": [ 95 | [1.0, 0.0, 0.0, 0.0], 96 | [0.0, 0.9, -0.02, 0.0], 97 | [0.0, 0.02, 0.9, -4.0], 98 | [0.0, 0.0, 0.0, 1.0] 99 | ], 100 | "registration_surface_filename": null, 101 | "transducer_body_filename": null 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/resources/example_db/transducers/example_transducer_array2/example_transducer_array2.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "TransducerArray", 3 | "id": "example_transducer_array", 4 | "name": "test example of an *array* of *transformed* transducers -- this time with certain attributes moved to attrs rather than individual modules", 5 | "modules": [ 6 | { 7 | "id": "module1", 8 | "name": "Module 1", 9 | "elements": [ 10 | { 11 | "index": 1, 12 | "position": [-17.5, 17.5, 0.0], 13 | "orientation": [0.0, 0.0, 0.0], 14 | "size": [3.7, 3.7], 15 | "pin": 1, 16 | "units": "mm" 17 | }, 18 | { 19 | "index": 2, 20 | "position": [-17.5, 13.5, 0.0], 21 | "orientation": [0.0, 0.0, 0.0], 22 | "size": [3.7, 3.7], 23 | "pin": 2, 24 | "units": "mm" 25 | } 26 | ], 27 | "frequency": 175000.0, 28 | "units": "mm", 29 | "attrs": {}, 30 | "registration_surface_filename": null, 31 | "transducer_body_filename": null, 32 | "standoff_transform": [ 33 | [1.0, 0.0, 0.0, 0.0], 34 | [0.0, 1.0, 0.0, 0.0], 35 | [0.0, 0.0, 1.0, 0.0], 36 | [0.0, 0.0, 0.0, 1.0] 37 | ], 38 | "sensitivity": 1200, 39 | "module_invert": [false], 40 | "transform": [ 41 | [0.9, 0.0, -0.2, 22.8], 42 | [0.0, 1.0, 0.0, 0.0], 43 | [0.2, 0.0, 0.9, 3.3], 44 | [0.0, 0.0, 0.0, 1.0] 45 | ] 46 | }, 47 | { 48 | "id": "module2", 49 | "name": "Module 2", 50 | "elements": [ 51 | { 52 | "index": 1, 53 | "position": [-17.5, 17.5, 0.0], 54 | "orientation": [0.0, 0.0, 0.0], 55 | "size": [3.7, 3.7], 56 | "pin": 1, 57 | "units": "mm" 58 | }, 59 | { 60 | "index": 2, 61 | "position": [-17.5, 13.5, 0.0], 62 | "orientation": [0.0, 0.0, 0.0], 63 | "size": [3.7, 3.7], 64 | "pin": 2, 65 | "units": "mm" 66 | } 67 | ], 68 | "frequency": 180000.0, 69 | "units": "mm", 70 | "attrs": {}, 71 | "registration_surface_filename": null, 72 | "transducer_body_filename": null, 73 | "standoff_transform": [ 74 | [1.0, 0.0, 0.0, 0.0], 75 | [0.0, 1.0, 0.0, 0.0], 76 | [0.0, 0.0, 1.0, 0.0], 77 | [0.0, 0.0, 0.0, 1.0] 78 | ], 79 | "sensitivity": 1000, 80 | "module_invert": [false], 81 | "transform": [ 82 | [0.9, 0.0, 0.2, -22.0], 83 | [0.0, 1.0, 0.0, 0.0], 84 | [-0.2, 0.0, 0.9, 4.2], 85 | [0.0, 0.0, 0.0, 1.0] 86 | ] 87 | } 88 | ], 89 | "attrs": { 90 | "standoff_transform": [ 91 | [1.0, 0.0, 0.0, 0.0], 92 | [0.0, 0.9, -0.02, 0.0], 93 | [0.0, 0.02, 0.9, -4.0], 94 | [0.0, 0.0, 0.0, 1.0] 95 | ], 96 | "impulse_response": [1000.0, 1000.0], 97 | "impulse_dt": 1, 98 | "registration_surface_filename": null, 99 | "transducer_body_filename": null 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /examples/legacy/demo.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/demo.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 hv_connected: 36 | console_version = interface.hvcontroller.get_version() 37 | print(f"Version: {console_version}") 38 | print("HV Controller connected.") 39 | interface.hvcontroller.set_voltage(24.5) 40 | interface.hvcontroller.turn_hv_on() 41 | input("Press Enter to continue...") 42 | 43 | interface.hvcontroller.turn_hv_off() 44 | 45 | 46 | 47 | if not tx_connected and not hv_connected: 48 | print("✅ LIFU Console not connected.") 49 | sys.exit(1) 50 | 51 | if not tx_connected: 52 | print("TX device not connected. Attempting to turn on 12V...") 53 | interface.hvcontroller.turn_12v_on() 54 | time.sleep(2) 55 | 56 | interface.stop_monitoring() 57 | del interface 58 | time.sleep(3) 59 | 60 | print("Reinitializing LIFU interface after powering 12V...") 61 | interface = LIFUInterface() 62 | tx_connected, hv_connected = interface.is_device_connected() 63 | 64 | if tx_connected and hv_connected: 65 | print("✅ LIFU Device fully connected.") 66 | else: 67 | print("❌ LIFU Device NOT fully connected.") 68 | print(f" TX Connected: {tx_connected}") 69 | print(f" HV Connected: {hv_connected}") 70 | sys.exit(1) 71 | 72 | print("HV Controller connected.") 73 | interface.hvcontroller.set_voltage(24.5) 74 | interface.hvcontroller.turn_hv_on() 75 | 76 | device_count = interface.txdevice.get_tx_module_count() 77 | print(f"Device Count: {device_count}") 78 | 79 | for i in range(1, device_count + 1): 80 | 81 | print("Ping the device") 82 | if not interface.txdevice.ping(module=i): 83 | print("❌ Failed comms with txdevice.") 84 | sys.exit(1) 85 | 86 | version = interface.txdevice.get_version(module=i) 87 | print(f"Version: {version}") 88 | 89 | 90 | if hv_connected: 91 | time.sleep(5) 92 | print("disabling HV for safety...") 93 | interface.hvcontroller.turn_hv_off() 94 | 95 | if __name__ == "__main__": 96 | main() 97 | -------------------------------------------------------------------------------- /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 pandas as pd 8 | import xarray as xa 9 | 10 | from openlifu.bf.apod_methods import ApodizationMethod 11 | from openlifu.geo import Point 12 | from openlifu.util.annotations import OpenLIFUFieldData 13 | from openlifu.util.units import getunittype 14 | from openlifu.xdc import Transducer 15 | 16 | 17 | @dataclass 18 | class PiecewiseLinear(ApodizationMethod): 19 | zero_angle: Annotated[float, OpenLIFUFieldData("Zero Apodization Angle", "Angle at and beyond which the piecewise linear apodization is 0%")] = 90.0 20 | """Angle at and beyond which the piecewise linear apodization is 0%""" 21 | 22 | rolloff_angle: Annotated[float, OpenLIFUFieldData("Rolloff start angle", "Angle below which the piecewise linear apodization is 100%")] = 45.0 23 | """Angle below which the piecewise linear apodization is 100%""" 24 | 25 | units: Annotated[str, OpenLIFUFieldData("Angle units", "Angle units")] = "deg" 26 | """Angle units""" 27 | 28 | def __post_init__(self): 29 | if not isinstance(self.zero_angle, (int, float)): 30 | raise TypeError(f"Zero angle must be a number, got {type(self.zero_angle).__name__}.") 31 | if self.zero_angle < 0: 32 | raise ValueError(f"Zero angle must be non-negative, got {self.zero_angle}.") 33 | if not isinstance(self.rolloff_angle, (int, float)): 34 | raise TypeError(f"Rolloff angle must be a number, got {type(self.rolloff_angle).__name__}.") 35 | if self.rolloff_angle < 0: 36 | raise ValueError(f"Rolloff angle must be non-negative, got {self.rolloff_angle}.") 37 | if self.rolloff_angle >= self.zero_angle: 38 | raise ValueError(f"Rolloff angle must be less than zero angle, got {self.rolloff_angle} >= {self.zero_angle}.") 39 | if getunittype(self.units) != "angle": 40 | raise ValueError(f"Units must be an angle type, got {self.units}.") 41 | 42 | def calc_apodization(self, arr: Transducer, target: Point, params: xa.Dataset, transform:np.ndarray | None=None): 43 | target_pos = target.get_position(units="m") 44 | matrix = transform if transform is not None else np.eye(4) 45 | angles = np.array([el.angle_to_point(target_pos, units="m", matrix=matrix, return_as=self.units) for el in arr.elements]) 46 | apod = np.zeros(arr.numelements()) 47 | f = ((self.zero_angle - angles) / (self.zero_angle - self.rolloff_angle)) 48 | apod = np.maximum(0, np.minimum(1, f)) 49 | return apod 50 | 51 | def to_table(self) -> pd.DataFrame: 52 | """ 53 | Get a table of the apodization method parameters 54 | 55 | :returns: Pandas DataFrame of the apodization method parameters 56 | """ 57 | records = [ 58 | {"Name": "Type", "Value": "Piecewise-Linear", "Unit": ""}, 59 | {"Name": "Zero Angle", "Value": self.zero_angle, "Unit": self.units}, 60 | {"Name": "Rolloff Angle", "Value": self.rolloff_angle, "Unit": self.units}, 61 | ] 62 | return pd.DataFrame.from_records(records) 63 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/verification/tst02_txmodule_selftest.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 | trigger_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.start_sonication(): 98 | print("Trigger Running Press enter to STOP:") 99 | input() # Wait for the user to press Enter 100 | if interface.stop_sonication(): 101 | print("Trigger stopped successfully.") 102 | else: 103 | print("Failed to stop trigger.") 104 | else: 105 | print("Failed to get trigger setting.") 106 | -------------------------------------------------------------------------------- /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 | sidelobe_to_mainlobe_pressure_ratio=[0.5/1.1, 0.6/1.2], # approx 0.45, 0.5 25 | sidelobe_to_mainlobe_intensity_ratio=[5.0/10.0, 5.5/12.0], # 0.5, approx 0.458 26 | global_pnp_MPa=[1.3], 27 | global_isppa_Wcm2=[13.0], 28 | p0_MPa=[1.0, 1.1], 29 | TIC=0.7, 30 | power_W=25.0, 31 | MI=1.2, 32 | global_ispta_mWcm2=540.0, 33 | param_constraints={ 34 | "global_pnp_MPa": ParameterConstraint( 35 | operator="<=", 36 | warning_value=1.4, 37 | error_value=1.6 38 | ) 39 | } 40 | ) 41 | 42 | def test_to_dict_from_dict_solution_analysis(example_solution_analysis: SolutionAnalysis): 43 | sa_dict = example_solution_analysis.to_dict() 44 | new_solution = SolutionAnalysis.from_dict(sa_dict) 45 | assert new_solution == example_solution_analysis 46 | 47 | @pytest.mark.parametrize("compact", [True, False]) 48 | def test_serialize_deserialize_solution_analysis(example_solution_analysis: SolutionAnalysis, compact: bool): 49 | json_str = example_solution_analysis.to_json(compact) 50 | deserialized = SolutionAnalysis.from_json(json_str) 51 | assert deserialized == example_solution_analysis 52 | 53 | 54 | # ---- Tests for SolutionAnalysisOptions ---- 55 | 56 | 57 | @pytest.fixture() 58 | def example_solution_analysis_options() -> SolutionAnalysisOptions: 59 | return SolutionAnalysisOptions( 60 | standoff_sound_speed=1480.0, 61 | standoff_density=990.0, 62 | ref_sound_speed=1540.0, 63 | ref_density=1020.0, 64 | mainlobe_aspect_ratio=(1.0, 1.0, 4.0), 65 | mainlobe_radius=2.0e-3, 66 | beamwidth_radius=4.0e-3, 67 | sidelobe_radius=2.5e-3, 68 | sidelobe_zmin=0.5e-3, 69 | distance_units="mm", 70 | param_constraints={ 71 | "mainlobe_radius": ParameterConstraint( 72 | operator=">=", 73 | warning_value=1.5e-3, 74 | error_value=1.0e-3 75 | ) 76 | } 77 | ) 78 | 79 | def test_to_dict_from_dict_solution_analysis_options(example_solution_analysis_options: SolutionAnalysisOptions): 80 | options_dict = example_solution_analysis_options.to_dict() 81 | new_options = SolutionAnalysisOptions.from_dict(options_dict) 82 | assert new_options == example_solution_analysis_options 83 | -------------------------------------------------------------------------------- /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 to_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_pulse_train_duration() 72 | else: 73 | interval = self.pulse_train_interval 74 | return interval * self.pulse_train_count 75 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | import pandas as pd 8 | 9 | from openlifu.util.annotations import OpenLIFUFieldData 10 | from openlifu.util.dict_conversion import DictMixin 11 | from openlifu.util.units import getunittype 12 | 13 | 14 | @dataclass 15 | class TargetConstraints(DictMixin): 16 | """A class for storing target constraints. 17 | 18 | Target constraints are used to define the acceptable range of 19 | positions for a target. For example, a target constraint could 20 | be used to define the acceptable range of values for the x position 21 | of a target. 22 | """ 23 | 24 | dim: Annotated[str, OpenLIFUFieldData("Constrained dimension ID", "The dimension ID being constrained")] = "x" 25 | """The dimension ID being constrained""" 26 | 27 | name: Annotated[str, OpenLIFUFieldData("Constrained dimension name", "The name of the dimension being constrained")] = "dim" 28 | """The name of the dimension being constrained""" 29 | 30 | units: Annotated[str, OpenLIFUFieldData("Dimension units", "The units of the dimension being constrained")] = "m" 31 | """The units of the dimension being constrained""" 32 | 33 | min: Annotated[float, OpenLIFUFieldData("Minimum allowed value", "The minimum value of the dimension")] = float("-inf") 34 | """The minimum value of the dimension""" 35 | 36 | max: Annotated[float, OpenLIFUFieldData("Maximum allowed value", "The maximum value of the dimension")] = float("inf") 37 | """The maximum value of the dimension""" 38 | 39 | def __post_init__(self): 40 | if not isinstance(self.dim, str): 41 | raise TypeError("Dimension ID must be a string") 42 | if not isinstance(self.name, str): 43 | raise TypeError("Dimension name must be a string") 44 | if not isinstance(self.units, str): 45 | raise TypeError("Dimension units must be a string") 46 | if getunittype(self.units) != 'distance': 47 | raise ValueError(f"Units must be a length unit, got {self.units}") 48 | if not isinstance(self.min, (int, float)): 49 | raise TypeError("Minimum value must be a number") 50 | if not isinstance(self.max, (int, float)): 51 | raise TypeError("Maximum value must be a number") 52 | if self.min > self.max: 53 | raise ValueError("Minimum value cannot be greater than maximum value") 54 | 55 | def check_bounds(self, pos: float): 56 | """Check if the given position is within bounds.""" 57 | 58 | if (pos < self.min) or (pos > self.max): 59 | logging.error(msg=f"The position {pos} at dimension {self.name} is not within bounds [{self.min}, {self.max}]!") 60 | raise ValueError(f"The position {pos} at dimension {self.name} is not within bounds [{self.min}, {self.max}]!") 61 | 62 | def to_table(self) -> pd.DataFrame: 63 | """ 64 | Get a table of the target constraints parameters. 65 | 66 | :returns: Pandas DataFrame of the target constraints parameters 67 | """ 68 | records = [ 69 | {"Name": self.name, "Value": f"({self.min},{self.max})", "Unit": self.units}, 70 | ] 71 | return pd.DataFrame.from_records(records) 72 | -------------------------------------------------------------------------------- /.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 | /logs 164 | /examples/tutorials/tutorial_database 165 | 166 | # ONNX checkpoints 167 | *.onnx 168 | 169 | notebooks/calibration_coeffs.py 170 | neg_cal.csv 171 | pos_cal.csv 172 | calibration_coeffs.py 173 | hv_calibration_coeffs.c 174 | hv_calibration_coeffs.h 175 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | "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 | "MI": { "operator": "<", "error_value": 1.9 } 88 | }, 89 | "target_constraints": [ 90 | { 91 | "dim": "x", 92 | "name": "Lateral", 93 | "units": "mm", 94 | "min": -100, 95 | "max": 100 96 | } 97 | ], 98 | "virtual_fit_options": { 99 | "units": "mm", 100 | "transducer_steering_center_distance": 50.0, 101 | "steering_limits": [ 102 | [-49, 47.5], 103 | [-51.2, 53], 104 | [-55, 58] 105 | ], 106 | "pitch_range": [-1, 120], 107 | "pitch_step": 3, 108 | "yaw_range": [-60, 66], 109 | "yaw_step": 2, 110 | "planefit_dyaw_extent": 14, 111 | "planefit_dyaw_step": 2, 112 | "planefit_dpitch_extent": 16, 113 | "planefit_dpitch_step": 7 114 | }, 115 | "analysis_options": { 116 | "standoff_sound_speed": 1500.0, 117 | "standoff_density": 1000.0, 118 | "ref_sound_speed": 1500.0, 119 | "ref_density": 1000.0, 120 | "mainlobe_aspect_ratio": [1.0, 1.0, 5.0], 121 | "mainlobe_radius": 2.5e-3, 122 | "beamwidth_radius": 5e-3, 123 | "sidelobe_radius": 3e-3, 124 | "sidelobe_zmin": 1e-3, 125 | "distance_units": "m" 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /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 | 74 | def test_from_dict_on_keyword_mismatch(): 75 | d = { 76 | "class": "UniformWater", 77 | "materials": { 78 | "water": { 79 | "name": "water", 80 | "sound_speed": 1500, 81 | "density": 1000, 82 | "attenuation": 0.0022, 83 | "specific_heat": 4182, 84 | "thermal_conductivity": 0.598 85 | } 86 | }, 87 | "ref_material": "water" 88 | } 89 | 90 | with pytest.raises(TypeError, match=r"Unexpected keyword arguments for UniformWater: \['ref_material'\]"): 91 | SegmentationMethod.from_dict(d, on_keyword_mismatch='raise') 92 | 93 | # This should not raise any warning or exception 94 | SegmentationMethod.from_dict(d, on_keyword_mismatch='ignore') 95 | -------------------------------------------------------------------------------- /examples/legacy/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", sensitivity=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 | dims = ds['p_max'].dims 91 | da = ds['p_max'].interp({dims[0]:np.arange(-30, 30.1, 0.5),dims[1]:np.arange(-30, 30.1, 0.5), dims[2]: np.arange(-4,70.1,0.5)}) 92 | origin_local = [float(val[0]) for dim, val in da.coords.items()] 93 | dx = [float(val[1]-val[0]) for dim, val in da.coords.items()] 94 | 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]) 95 | data = da.data 96 | im = nb.Nifti1Image(data, affine) 97 | h = im.header 98 | h.set_xyzt_units('mm', 'sec') 99 | im = nb.as_closest_canonical(im) 100 | im.to_filename(output_filename) 101 | 102 | 103 | # %% 104 | # Load into Slicer 105 | import slicerio.server 106 | 107 | slicerio.server.file_load(output_filename, slicer_executable=slicer_exe) 108 | -------------------------------------------------------------------------------- /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": { 6 | "id": "example_transducer", 7 | "name": "Example Transducer", 8 | "elements": [ 9 | { 10 | "index": 1, 11 | "position": [-14, -14, 0], 12 | "units": "m" 13 | }, 14 | { 15 | "index": 2, 16 | "position": [-2, -2, 0], 17 | "units": "m" 18 | }, 19 | { 20 | "index": 3, 21 | "position": [2, 2, 0], 22 | "units": "m" 23 | }, 24 | { 25 | "index": 4, 26 | "position": [14, 14, 0], 27 | "units": "m" 28 | } 29 | ], 30 | "frequency": 1e6, 31 | "units": "m" 32 | }, 33 | "date_created": "2024-01-30T09:18:11", 34 | "description": "Example plan created 30-Jan-2024 09:16:02", 35 | "delays": [ 36 | [ 37 | 7.139258974920841e-7, 1.164095583074107e-6, 1.4321977043056202e-6, 38 | 1.5143736925332023e-6, 1.4094191657896087e-6, 1.1188705305994071e-6, 39 | 6.46894718323139e-7, 0.0, 1.2683760653622907e-6, 1.7251583088871662e-6, 40 | 1.9972746364594766e-6, 2.0806926330138847e-6, 1.974152793990993e-6, 41 | 1.6792617729461087e-6, 1.2003736682835394e-6, 5.442792226777689e-7, 42 | 1.6425243839760022e-6, 2.1038804054306437e-6, 2.378775260158848e-6, 43 | 2.4630532471222807e-6, 2.3554157329476133e-6, 2.0575192329568598e-6, 44 | 1.5738505533232074e-6, 9.114003043615498e-7, 1.8309933020916417e-6, 45 | 2.29468834620898e-6, 2.571004767271362e-6, 2.655722845121427e-6, 46 | 2.5475236155907678e-6, 2.248089500472459e-6, 1.7619762095269915e-6, 47 | 1.0962779929139644e-6, 1.8309933020916417e-6, 2.29468834620898e-6, 48 | 2.571004767271362e-6, 2.655722845121427e-6, 2.5475236155907678e-6, 49 | 2.248089500472459e-6, 1.7619762095269915e-6, 1.0962779929139644e-6, 50 | 1.6425243839760022e-6, 2.1038804054306437e-6, 2.378775260158848e-6, 51 | 2.4630532471222807e-6, 2.3554157329476133e-6, 2.0575192329568598e-6, 52 | 1.5738505533232074e-6, 9.114003043615498e-7, 1.2683760653622907e-6, 53 | 1.7251583088871662e-6, 1.9972746364594766e-6, 2.0806926330138847e-6, 54 | 1.974152793990993e-6, 1.6792617729461087e-6, 1.2003736682835394e-6, 55 | 5.442792226777689e-7, 7.139258974920841e-7, 1.164095583074107e-6, 56 | 1.4321977043056202e-6, 1.5143736925332023e-6, 1.4094191657896087e-6, 57 | 1.1188705305994071e-6, 6.46894718323139e-7, 0.0 58 | ] 59 | ], 60 | "apodizations": [ 61 | [ 62 | 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, 63 | 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, 64 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 65 | ] 66 | ], 67 | "pulse": { 68 | "frequency": 500000, 69 | "amplitude": 1.0, 70 | "duration": 2e-5 71 | }, 72 | "voltage": 2.3167290687561035, 73 | "sequence": { 74 | "pulse_interval": 0.1, 75 | "pulse_count": 10, 76 | "pulse_train_interval": 1, 77 | "pulse_train_count": 1 78 | }, 79 | "foci": [ 80 | { 81 | "id": "example_target", 82 | "name": "Example Target", 83 | "color": [1.0, 0.0, 0.0], 84 | "radius": 0.001, 85 | "position": [0.0, -0.0022437460888595447, 0.05518120697745499], 86 | "dims": ["x", "y", "z"], 87 | "units": "m" 88 | } 89 | ], 90 | "target": { 91 | "id": "example_target", 92 | "name": "Example Target", 93 | "color": [1.0, 0.0, 0.0], 94 | "radius": 0.001, 95 | "position": [0.0, -0.0022437460888595447, 0.05518120697745499], 96 | "dims": ["x", "y", "z"], 97 | "units": "m" 98 | }, 99 | "approved": false 100 | } 101 | -------------------------------------------------------------------------------- /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 | import pandas as pd 8 | 9 | from openlifu.bf.focal_patterns import FocalPattern 10 | from openlifu.geo import Point 11 | from openlifu.util.annotations import OpenLIFUFieldData 12 | 13 | 14 | @dataclass 15 | class Wheel(FocalPattern): 16 | """ 17 | Class for representing a wheel pattern 18 | """ 19 | 20 | center: Annotated[bool, OpenLIFUFieldData("Include center point?", "Whether to include the center for the wheel pattern")] = True 21 | """Whether to include the center for the wheel pattern""" 22 | 23 | num_spokes: Annotated[int, OpenLIFUFieldData("Number of spokes", "Number of spokes in the wheel pattern")] = 4 24 | """Number of spokes in the wheel pattern""" 25 | 26 | spoke_radius: Annotated[float, OpenLIFUFieldData("Spoke radius", "Radius of the spokes in the wheel pattern")] = 1.0 # mm 27 | """Radius of the spokes in the wheel pattern""" 28 | 29 | distance_units: Annotated[str, OpenLIFUFieldData("Units", "Units of the wheel pattern parameters")] = "mm" 30 | """Units of the wheel pattern parameters""" 31 | 32 | def __post_init__(self): 33 | if not isinstance(self.center, bool): 34 | raise TypeError(f"Center must be a boolean, got {type(self.center).__name__}.") 35 | if not isinstance(self.num_spokes, int) or self.num_spokes < 1: 36 | raise ValueError(f"Number of spokes must be a positive integer, got {self.num_spokes}.") 37 | if not isinstance(self.spoke_radius, (int, float)) or self.spoke_radius <= 0: 38 | raise ValueError(f"Spoke radius must be a positive number, got {self.spoke_radius}.") 39 | super().__post_init__() 40 | 41 | def get_targets(self, target: Point): 42 | """ 43 | Get the targets of the focal pattern 44 | 45 | :param target: Target point of the focal pattern 46 | :returns: List of target points 47 | """ 48 | if self.center: 49 | targets = [target.copy()] 50 | targets[0].id = f"{target.id}_center" 51 | targets[0].id = f"{target.id} (Center)" 52 | else: 53 | targets = [] 54 | m = target.get_matrix(center_on_point=True) 55 | for i in range(self.num_spokes): 56 | theta = 2*np.pi*i/self.num_spokes 57 | local_position = self.spoke_radius * np.array([np.cos(theta), np.sin(theta), 0.0]) 58 | position = np.dot(m, np.append(local_position, 1.0))[:3] 59 | spoke = Point(id=f"{target.id}_{np.rad2deg(theta):.0f}deg", 60 | name=f"{target.name} ({np.rad2deg(theta):.0f}°)", 61 | position=position, 62 | units=self.distance_units, 63 | radius=target.radius) 64 | targets.append(spoke) 65 | return targets 66 | 67 | def num_foci(self) -> int: 68 | """ 69 | Get the number of foci in the focal pattern 70 | 71 | :returns: Number of foci 72 | """ 73 | return int(self.center) + self.num_spokes 74 | 75 | def to_table(self) -> pd.DataFrame: 76 | """ 77 | Get a table of the focal pattern parameters 78 | 79 | :returns: Pandas DataFrame of the focal pattern parameters 80 | """ 81 | records = [ 82 | {"Name": "Type", "Value": "Wheel", "Unit": ""}, 83 | {"Name": "Target Pressure", "Value": self.target_pressure, "Unit": self.units}, 84 | {"Name": "Center", "Value": self.center, "Unit": ""}, 85 | {"Name": "Number of Spokes", "Value": self.num_spokes, "Unit": ""}, 86 | {"Name": "Spoke Radius", "Value": self.spoke_radius, "Unit": self.distance_units}, 87 | ] 88 | return pd.DataFrame.from_records(records) 89 | -------------------------------------------------------------------------------- /examples/legacy/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 | -------------------------------------------------------------------------------- /examples/legacy/test_async.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import asyncio 4 | import logging 5 | import threading 6 | import time 7 | 8 | from openlifu.io.LIFUInterface import LIFUInterface 9 | 10 | # set PYTHONPATH=%cd%\src;%PYTHONPATH% 11 | # python notebooks/test_async.py 12 | 13 | # Setup logging 14 | logging.basicConfig(level=logging.INFO) 15 | 16 | interface = None 17 | 18 | # Callbacks 19 | def on_connect(descriptor, port): 20 | print(f"🔌 CONNECTED: {descriptor} on port {port}") 21 | 22 | def on_disconnect(descriptor, port): 23 | print(f"❌ DISCONNECTED: {descriptor} from port {port}") 24 | 25 | def on_data_received(descriptor, packet): 26 | print(f"📦 DATA [{descriptor}]: {packet}") 27 | 28 | def monitor_interface(): 29 | """Run the device monitor loop in a separate thread using asyncio.""" 30 | asyncio.run(interface.start_monitoring(interval=1)) 31 | 32 | def rebind_tx_callbacks(): 33 | """Bind callbacks to the TX UART, if present.""" 34 | if interface.txdevice and interface.txdevice.uart: 35 | interface.txdevice.uart.signal_connect.connect(on_connect) 36 | interface.txdevice.uart.signal_disconnect.connect(on_disconnect) 37 | interface.txdevice.uart.signal_data_received.connect(on_data_received) 38 | 39 | def run_menu(): 40 | while True: 41 | print("\n--- LIFU MENU ---") 42 | print("1. Turn ON 12V") 43 | print("2. Turn OFF 12V") 44 | print("3. Ping TX") 45 | print("4. Show Connection Status") 46 | print("5. Exit") 47 | choice = input("Enter choice: ").strip() 48 | 49 | tx_connected, hv_connected = interface.is_device_connected() 50 | 51 | if choice == "1": 52 | if hv_connected: 53 | print("⚡ Sending 12V ON...") 54 | interface.hvcontroller.turn_12v_on() 55 | time.sleep(2.0) 56 | print("🔄 Reinitializing TX...") 57 | rebind_tx_callbacks() 58 | else: 59 | print("⚠️ HV not connected.") 60 | 61 | elif choice == "2": 62 | if hv_connected: 63 | print("🛑 Sending 12V OFF...") 64 | interface.hvcontroller.turn_12v_off() 65 | else: 66 | print("⚠️ HV not connected.") 67 | 68 | elif choice == "3": 69 | if tx_connected: 70 | print("📡 Sending PING to TX...") 71 | resp = interface.txdevice.ping() 72 | if resp: 73 | print("✅ TX responded to PING.") 74 | else: 75 | print("❌ No response or error.") 76 | else: 77 | print("⚠️ TX not connected.") 78 | 79 | elif choice == "4": 80 | print("Status:") 81 | print(f" TX: {'✅ Connected' if tx_connected else '❌ Not connected'}") 82 | print(f" HV: {'✅ Connected' if hv_connected else '❌ Not connected'}") 83 | 84 | elif choice == "5": 85 | print("Exiting...") 86 | interface.stop_monitoring() 87 | break 88 | else: 89 | print("Invalid choice.") 90 | 91 | if __name__ == "__main__": 92 | interface = LIFUInterface(HV_test_mode=False, run_async=False) 93 | 94 | # Bind callbacks for HV and (initially connected) TX 95 | if interface.hvcontroller.uart: 96 | interface.hvcontroller.uart.signal_connect.connect(on_connect) 97 | interface.hvcontroller.uart.signal_disconnect.connect(on_disconnect) 98 | interface.hvcontroller.uart.signal_data_received.connect(on_data_received) 99 | 100 | rebind_tx_callbacks() 101 | 102 | print("🔍 Starting LIFU monitoring...") 103 | monitor_thread = threading.Thread(target=monitor_interface, daemon=True) 104 | monitor_thread.start() 105 | 106 | try: 107 | run_menu() 108 | except KeyboardInterrupt: 109 | print("\n🛑 Stopped by user.") 110 | interface.stop_monitoring() 111 | -------------------------------------------------------------------------------- /examples/legacy/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.start_sonication(): 59 | print("Trigger Running Press enter to STOP:") 60 | input() # Wait for the user to press Enter 61 | if interface.stop_sonication(): 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.start_sonication(): 88 | print("Trigger Running Press enter to STOP:") 89 | input() # Wait for the user to press Enter 90 | if interface.stop_sonication(): 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /examples/verification/tst01_console_selftest.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 | -------------------------------------------------------------------------------- /examples/tools/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 logging 19 | import pathlib 20 | import shutil 21 | import sys 22 | import tempfile 23 | 24 | from openlifu.db import Database 25 | from openlifu.db.database import OnConflictOpts 26 | from openlifu.xdc import Transducer 27 | 28 | if len(sys.argv) != 2: 29 | raise RuntimeError("Provide exactly one argument: the path to the database folder.") 30 | db = Database(sys.argv[1]) 31 | 32 | db.write_protocol_ids(db.get_protocol_ids()) 33 | for protocol_id in db.get_protocol_ids(): 34 | protocol = db.load_protocol(protocol_id) 35 | assert protocol_id == protocol.id 36 | db.write_protocol(protocol, on_conflict=OnConflictOpts.OVERWRITE) 37 | 38 | db.write_transducer_ids(db.get_transducer_ids()) 39 | for transducer_id in db.get_transducer_ids(): 40 | transducer = db.load_transducer(transducer_id, convert_array=False) 41 | if not isinstance(transducer, Transducer): 42 | logging.warning(f"Skipping {transducer_id} because TransducerArray writing is not supported.") 43 | continue 44 | assert transducer_id == transducer.id 45 | db.write_transducer(transducer, on_conflict=OnConflictOpts.OVERWRITE) 46 | 47 | db.write_subject_ids(db.get_subject_ids()) 48 | for subject_id in db.get_subject_ids(): 49 | subject = db.load_subject(subject_id) 50 | assert subject_id == subject.id 51 | db.write_subject(subject, on_conflict=OnConflictOpts.OVERWRITE) 52 | 53 | db.write_volume_ids(subject_id, db.get_volume_ids(subject_id)) 54 | for volume_id in db.get_volume_ids(subject_id): 55 | volume_info = db.get_volume_info(subject_id, volume_id) 56 | assert volume_info["id"] == volume_id 57 | volume_data_abspath = pathlib.Path(volume_info["data_abspath"]) 58 | 59 | # The weird file move here is because of a quirk in Database: 60 | # - you can't just edit the volume metadata, you have to write the metadata json and volume data file together 61 | # - if you try to provide the volume_data_abspath as the data path you get a SameFileError from shutil which 62 | # 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. 63 | with tempfile.TemporaryDirectory() as tmpdir: 64 | tmpdir = pathlib.Path(tmpdir) 65 | moved_vol_abspath = tmpdir / volume_data_abspath.name 66 | shutil.move(volume_data_abspath, moved_vol_abspath) 67 | db.write_volume(subject_id, volume_id, volume_info["name"], moved_vol_abspath, on_conflict=OnConflictOpts.OVERWRITE) 68 | 69 | session_ids = db.get_session_ids(subject.id) 70 | db.write_session_ids(subject_id, session_ids) 71 | for session_id in session_ids: 72 | session = db.load_session(subject, session_id) 73 | assert session.id == session_id 74 | assert session.subject_id == subject.id 75 | db.write_session(subject, session, on_conflict=OnConflictOpts.OVERWRITE) 76 | 77 | solution_ids = db.get_solution_ids(session.subject_id, session.id) 78 | db.write_solution_ids(session, solution_ids) 79 | for solution_id in solution_ids: 80 | solution = db.load_solution(session, solution_id) 81 | assert solution.id == solution_id 82 | assert solution.simulation_result['p_min'].shape[0] == solution.num_foci() 83 | db.write_solution(session, solution, on_conflict=OnConflictOpts.OVERWRITE) 84 | 85 | run_ids = db.get_run_ids(subject_id, session_id) 86 | db.write_run_ids(subject_id, session_id, run_ids) 87 | # (Runs are read only at the moment so it's just the runs.json and no individual runs to standardize) 88 | 89 | db.write_user_ids(db.get_user_ids()) 90 | for user_id in db.get_user_ids(): 91 | user = db.load_user(user_id) 92 | assert user_id == user.id 93 | db.write_user(user, on_conflict=OnConflictOpts.OVERWRITE) 94 | -------------------------------------------------------------------------------- /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, TransducerArray 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 | def load_transducer_array(transducer_array_id : str) -> TransducerArray: 17 | """Load an example TransducerArray given the transducer ID.""" 18 | return TransducerArray.from_file(Path(__file__).parent/f'resources/example_db/transducers/{transducer_array_id}/{transducer_array_id}.json') 19 | 20 | @pytest.mark.parametrize("compact_representation", [True, False]) 21 | def test_serialize_deserialize_transducer(example_transducer : Transducer, compact_representation: bool): 22 | reconstructed_transducer = example_transducer.from_json(example_transducer.to_json(compact_representation)) 23 | dataclasses_are_equal(example_transducer, reconstructed_transducer) 24 | 25 | def test_get_polydata_color_options(example_transducer : Transducer): 26 | """Ensure that the color is set correctly on the polydata""" 27 | polydata_with_default_color = example_transducer.get_polydata() 28 | point_scalars = polydata_with_default_color.GetPointData().GetScalars() 29 | assert point_scalars is None 30 | 31 | polydata_with_given_color = example_transducer.get_polydata(facecolor=[0,1,1,0.5]) 32 | point_scalars = polydata_with_given_color.GetPointData().GetScalars() 33 | assert point_scalars is not None 34 | 35 | def test_default_transducer(): 36 | """Ensure it is possible to construct a default transducer""" 37 | Transducer() 38 | 39 | def test_convert_transform(): 40 | transducer = Transducer(units='cm') 41 | transform = transducer.convert_transform( 42 | matrix = np.array([ 43 | [1,0,0,2], 44 | [0,1,0,3], 45 | [0,0,1,4], 46 | [0,0,0,1], 47 | ], dtype=float), 48 | units = "m", 49 | ) 50 | expected_transform = np.array([ 51 | [1,0,0,200], 52 | [0,1,0,300], 53 | [0,0,1,400], 54 | [0,0,0,1], 55 | ], dtype=float) 56 | assert np.allclose(transform,expected_transform) 57 | 58 | def test_get_effective_origin(): 59 | transducer = Transducer.gen_matrix_array(nx=3, ny=2, units='cm') 60 | effective_origin_with_all_active = transducer.get_effective_origin(apodizations = np.ones(transducer.numelements())) 61 | assert np.allclose(effective_origin_with_all_active, np.zeros(3)) 62 | 63 | rng = np.random.default_rng() 64 | element_index_to_turn_on = rng.integers(transducer.numelements()) 65 | apodizations_with_just_one_element = np.zeros(transducer.numelements()) 66 | apodizations_with_just_one_element[element_index_to_turn_on] = 0.5 # It is allowed to be a number between 0 and 1 67 | assert np.allclose( 68 | transducer.get_effective_origin(apodizations = apodizations_with_just_one_element, units = "um"), 69 | transducer.get_positions(units="um")[element_index_to_turn_on], 70 | ) 71 | 72 | def test_get_standoff_transform_in_units(): 73 | standoff_transform_in_mm = np.array([ 74 | [-0.1,0.9,0,20], 75 | [0.9,0.1,0,30], 76 | [0,0,1,40], 77 | [0,0,0,1], 78 | ]) 79 | standoff_transform_in_cm = np.array([ 80 | [-0.1,0.9,0,2], 81 | [0.9,0.1,0,3], 82 | [0,0,1,4], 83 | [0,0,0,1], 84 | ]) 85 | transducer = Transducer(units='mm') 86 | transducer.standoff_transform = standoff_transform_in_mm 87 | assert np.allclose( 88 | transducer.get_standoff_transform_in_units("cm"), 89 | standoff_transform_in_cm, 90 | ) 91 | 92 | def test_read_data_types(example_transducer:Transducer): 93 | assert isinstance(example_transducer.standoff_transform, np.ndarray) 94 | if len(example_transducer.elements) > 0: 95 | assert isinstance(example_transducer.elements[0], Element) 96 | 97 | @pytest.mark.parametrize( 98 | "transducer_array_id", 99 | [ 100 | "example_transducer_array", 101 | "example_transducer_array2", 102 | ] 103 | ) 104 | def test_transducer_array_to_transducer_data_types(transducer_array_id): 105 | transducer_array : TransducerArray = load_transducer_array(transducer_array_id) 106 | transducer = transducer_array.to_transducer() 107 | assert isinstance(transducer.standoff_transform, np.ndarray) 108 | assert isinstance(transducer.impulse_response, np.ndarray) 109 | if len(transducer.elements) > 0: 110 | assert isinstance(transducer.elements[0], Element) 111 | --------------------------------------------------------------------------------