diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000..3c05b71 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,55 @@ +# Deploy Documenter.jl output to GitHub Pages (free for public repos). +# +# One-time setup in the repository on GitHub: +# Settings → Pages → Build and deployment → Source → GitHub Actions + +name: Deploy Documentation + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Julia + uses: julia-actions/setup-julia@v2 + with: + version: "1.12" + + - name: Cache Julia artifacts + uses: julia-actions/cache@v2 + + - name: Build documentation + run: julia --project=docs --color=yes docs/make.jl + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v4 + with: + path: docs/build/ + + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..05d09b3 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,73 @@ +name: Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + +permissions: + contents: read + +jobs: + test: + name: Julia 1.12 + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Julia + uses: julia-actions/setup-julia@v2 + with: + version: "1.12" + + - name: Cache Julia artifacts + uses: julia-actions/cache@v2 + + - name: Instantiate project + run: julia --project=. --color=yes -e 'using Pkg; Pkg.instantiate()' + + - name: Run test suite + run: julia --project=. --color=yes -e 'using Pkg; Pkg.test(; coverage=true)' + + - name: Generate coverage file + run: julia --color=yes coverage.jl + + - name: Upload coverage file + uses: actions/upload-artifact@v4 + with: + name: coverage-lcov + path: | + coverage/lcov.info + coverage/summary.txt + if-no-files-found: error + + docs: + name: Documentation + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Julia + uses: julia-actions/setup-julia@v2 + with: + version: "1.12" + + - name: Cache Julia artifacts + uses: julia-actions/cache@v2 + + - name: Build documentation + run: julia --project=docs --color=yes docs/make.jl + + - name: Upload documentation site + uses: actions/upload-artifact@v4 + with: + name: documentation-site + path: docs/build/ + if-no-files-found: error diff --git a/.gitignore b/.gitignore index a364377..0bbbda4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ LocalPreferences.toml *.jl.cov *.jl.*.cov *.jl.mem +coverage/ .tmp_pdfenv @@ -13,6 +14,7 @@ LocalPreferences.toml # Generated outputs from the project scripts outputs/ +docs/build/ *.jld2 diff --git a/Manifest.toml b/Manifest.toml index fc396c6..6b5ab9e 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -1,8 +1,8 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.12.6" +julia_version = "1.12.2" manifest_format = "2.0" -project_hash = "843780e8d2ff49e8cdec16a219ae2d90dc8d5ac0" +project_hash = "24446f29591bbc15a7a25026a446738be8a67088" [[deps.AbstractFFTs]] deps = ["LinearAlgebra"] @@ -601,6 +601,24 @@ git-tree-sha1 = "53bb909d1151e57e2484c3d1b53e19552b887fb2" uuid = "42e2da0e-8278-4e71-bc24-59509adca0fe" version = "1.0.2" +[[deps.HDF5]] +deps = ["Compat", "HDF5_jll", "Libdl", "MPIPreferences", "Mmap", "Preferences", "Printf", "Random", "Requires", "UUIDs"] +git-tree-sha1 = "491ea627ac824619f34168e29a0427a9e00e3e40" +uuid = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f" +version = "0.17.3" + + [deps.HDF5.extensions] + MPIExt = "MPI" + + [deps.HDF5.weakdeps] + MPI = "da04e1cc-30fd-572f-bb4f-1f8673147195" + +[[deps.HDF5_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "LibCURL_jll", "Libdl", "MPIABI_jll", "MPICH_jll", "MPIPreferences", "MPItrampoline_jll", "MicrosoftMPI_jll", "OpenMPI_jll", "OpenSSL_jll", "TOML", "Zlib_jll", "aws_c_s3_jll", "dlfcn_win32_jll", "libaec_jll", "mpif_jll"] +git-tree-sha1 = "45337643a2d97262d5fe72ce1f13e8a662d13d62" +uuid = "0234f1f7-429e-5d53-9886-15a909be8d59" +version = "2.1.2+0" + [[deps.HarfBuzz_jll]] deps = ["Artifacts", "Cairo_jll", "Fontconfig_jll", "FreeType2_jll", "Glib_jll", "Graphite2_jll", "JLLWrappers", "Libdl", "Libffi_jll"] git-tree-sha1 = "f923f9a774fcf3f5cb761bfa43aeadd689714813" @@ -612,6 +630,12 @@ git-tree-sha1 = "2eaa69a7cab70a52b9687c8bf950a5a93ec895ae" uuid = "076d061b-32b6-4027-95e0-9a2c6f6d7e74" version = "0.2.0" +[[deps.Hwloc_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "XML2_jll", "Xorg_libpciaccess_jll"] +git-tree-sha1 = "baaaebd42ed9ee1bd9173cfd56910e55a8622ee1" +uuid = "e33a78d0-f292-5ffc-b300-72abe9b543c8" +version = "2.13.0+1" + [[deps.HypergeometricFunctions]] deps = ["LinearAlgebra", "OpenLibm_jll", "SpecialFunctions"] git-tree-sha1 = "68c173f4f449de5b438ee67ed0c9c748dc31a2ec" @@ -1007,6 +1031,30 @@ git-tree-sha1 = "282cadc186e7b2ae0eeadbd7a4dffed4196ae2aa" uuid = "856f044c-d86e-5d09-b602-aeab76dc8ba7" version = "2025.2.0+0" +[[deps.MPIABI_jll]] +deps = ["Artifacts", "Hwloc_jll", "JLLWrappers", "LazyArtifacts", "Libdl", "MPIPreferences", "TOML"] +git-tree-sha1 = "9be143b6045719e8fb019d2b3bc2aebad1184fef" +uuid = "b5ada748-db0f-5fc0-8972-9331c762740c" +version = "0.1.5+0" + +[[deps.MPICH_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Hwloc_jll", "JLLWrappers", "Libdl", "MPIPreferences", "TOML"] +git-tree-sha1 = "07dbec8aab01696edc0151a401a6cdfe95b9b885" +uuid = "7cb0a576-ebde-5e09-9194-50597f1243b4" +version = "5.0.1+0" + +[[deps.MPIPreferences]] +deps = ["Libdl", "Preferences"] +git-tree-sha1 = "8e98d5d80b87403c311fd51e8455d4546ba7a5f8" +uuid = "3da0fdf6-3ccc-4f1b-acd9-58baa6c99267" +version = "0.1.12" + +[[deps.MPItrampoline_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "LazyArtifacts", "Libdl", "MPIPreferences", "TOML"] +git-tree-sha1 = "675df097f8eeb28998b2cfe3b25655af73d5f7df" +uuid = "f1f71cc9-e9ae-5b93-9b94-4fe0e1ad3748" +version = "5.5.6+0" + [[deps.MacroTools]] git-tree-sha1 = "1e0228a030642014fe5cfe68c2c0a818f9e3f522" uuid = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" @@ -1046,6 +1094,12 @@ git-tree-sha1 = "535656ce55266bfed0575cd051acc4f36dc869a0" uuid = "0b3b1443-0f03-428d-bdfb-f27f9c1191ea" version = "0.1.15" +[[deps.MicrosoftMPI_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] +git-tree-sha1 = "bc95bf4149bf535c09602e3acdf950d9b4376227" +uuid = "9237b28f-5490-5468-be7b-bb81f5f5e6cf" +version = "10.1.4+3" + [[deps.Missings]] deps = ["DataAPI"] git-tree-sha1 = "ec4f7fbeab05d7747bdf98eb74d130a2a2ed298d" @@ -1064,7 +1118,7 @@ version = "0.3.4" [[deps.MozillaCACerts_jll]] uuid = "14a3606d-f60d-562e-9121-12d972cd8159" -version = "2025.11.4" +version = "2025.5.20" [[deps.MuladdMacro]] git-tree-sha1 = "cac9cc5499c25554cba55cd3c30543cff5ca4fab" @@ -1151,6 +1205,12 @@ deps = ["Artifacts", "Libdl"] uuid = "05823500-19ac-5b8b-9628-191a04bc5112" version = "0.8.7+0" +[[deps.OpenMPI_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Hwloc_jll", "JLLWrappers", "LazyArtifacts", "Libdl", "MPIPreferences", "TOML", "Zlib_jll"] +git-tree-sha1 = "6d6c0ca4824268c1a7dca1f4721c535ac63d9074" +uuid = "fe0851c0-eecd-5654-98d4-656369965a5c" +version = "5.0.11+0" + [[deps.OpenSSL_jll]] deps = ["Artifacts", "Libdl"] uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" @@ -1233,7 +1293,7 @@ version = "0.44.2+0" [[deps.Pkg]] deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"] uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -version = "1.12.1" +version = "1.12.0" weakdeps = ["REPL"] [deps.Pkg.extensions] @@ -1648,7 +1708,7 @@ uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" version = "0.11.3" [[deps.TranscranialFUS]] -deps = ["CUDA", "CairoMakie", "CondaPkg", "DICOM", "FFTW", "Interpolations", "JLD2", "JSON3", "PythonCall", "Random", "Statistics"] +deps = ["CUDA", "CairoMakie", "CondaPkg", "DICOM", "Dates", "FFTW", "HDF5", "Interpolations", "JLD2", "JSON3", "Printf", "PythonCall", "Random", "Statistics"] path = "." uuid = "a2de8f33-df8e-4198-bbcc-8a183cd2f9f0" version = "0.1.0" @@ -1722,6 +1782,12 @@ git-tree-sha1 = "248a7031b3da79a127f14e5dc5f417e26f9f6db7" uuid = "efce3f68-66dc-5838-9240-27a6d6f5f9b6" version = "1.1.0" +[[deps.XML2_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "Libiconv_jll", "Zlib_jll"] +git-tree-sha1 = "80d3930c6347cfce7ccf96bd3bafdf079d9c0390" +uuid = "02c8fc9c-b97f-50b9-bbe4-9be30ff0a78a" +version = "2.13.9+0" + [[deps.XZ_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] git-tree-sha1 = "9cce64c0fdd1960b597ba7ecda2950b5ed957438" @@ -1793,18 +1859,84 @@ git-tree-sha1 = "446b23e73536f84e8037f5dce465e92275f6a308" uuid = "3161d3a3-bdf6-5164-811a-617609db77b4" version = "1.5.7+1" +[[deps.aws_c_auth_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "aws_c_cal_jll", "aws_c_http_jll", "aws_c_sdkutils_jll"] +git-tree-sha1 = "8cab83c96af80a1be968251ce1a0548a7545484d" +uuid = "2b3700d1-4306-52e2-a478-c162f0c514be" +version = "0.9.6+0" + +[[deps.aws_c_cal_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "aws_c_common_jll"] +git-tree-sha1 = "22c0f42f4a1f0dc5dcfa8fd267c4ac407c455e7a" +uuid = "70f11efc-bab2-57f1-b0f3-22aad4e67c4b" +version = "0.9.13+0" + +[[deps.aws_c_common_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "a759cb9bf456ad792cc7898a81ae333cce9ef02a" +uuid = "73048d1d-b8c4-5092-a58d-866c5e8d1e50" +version = "0.12.6+0" + +[[deps.aws_c_compression_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "aws_c_common_jll"] +git-tree-sha1 = "7910c72f45f44afd297c39fe43b99c56d5ed22ec" +uuid = "73a04cd5-f3d7-5bac-9290-e8adb709f224" +version = "0.3.2+0" + +[[deps.aws_c_http_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "aws_c_compression_jll", "aws_c_io_jll"] +git-tree-sha1 = "e358d5a001ef7afbd4f8c5225322512819cda2f2" +uuid = "3254fc65-9028-534d-aa9d-d76d128babc6" +version = "0.10.13+0" + +[[deps.aws_c_io_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "aws_c_cal_jll", "aws_c_common_jll", "s2n_tls_jll"] +git-tree-sha1 = "7e481d474b2087ee8bbf55b81bf9119f21e396d9" +uuid = "13c41daa-f319-5298-b5eb-5754e0170d52" +version = "0.26.3+0" + +[[deps.aws_c_s3_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "aws_c_auth_jll", "aws_c_common_jll", "aws_c_http_jll", "aws_checksums_jll", "s2n_tls_jll"] +git-tree-sha1 = "3e9917ab25114feba657e71be41cad068b9f6595" +uuid = "bd1f34fb-993f-5903-a121-aaf302eed6d4" +version = "0.11.5+0" + +[[deps.aws_c_sdkutils_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "aws_c_common_jll"] +git-tree-sha1 = "c43dfba2c1ab9ea9f02f2c80e86fa16f6460244e" +uuid = "1282aa60-004d-510b-9f52-12498d409daa" +version = "0.2.4+1" + +[[deps.aws_checksums_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "aws_c_common_jll"] +git-tree-sha1 = "2570c8e23f4771a087b12a47edcaaa670ac05a01" +uuid = "b2a88e68-78e7-5e94-8c20-c02986ec140e" +version = "0.2.10+0" + [[deps.demumble_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] git-tree-sha1 = "6498e3581023f8e530f34760d18f75a69e3a4ea8" uuid = "1e29f10c-031c-5a83-9565-69cddfc27673" version = "1.3.0+0" +[[deps.dlfcn_win32_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "e141d67ffe550eadfb5af1bdbdaf138031e4805f" +uuid = "c4b69c83-5512-53e3-94e6-de98773c479f" +version = "1.4.2+0" + [[deps.isoband_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] git-tree-sha1 = "51b5eeb3f98367157a7a12a1fb0aa5328946c03c" uuid = "9a68df92-36a6-505f-a73e-abb412b6bfb4" version = "0.2.3+0" +[[deps.libaec_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "1411bc34c180946d3cef591de1384012afa6edee" +uuid = "477f73a3-ac25-53e9-8cc3-50b2fa2566f0" +version = "1.1.6+0" + [[deps.libaom_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] git-tree-sha1 = "371cc681c00a3ccc3fbc5c0fb91f58ba9bec1ecf" @@ -1870,6 +2002,12 @@ git-tree-sha1 = "717df6f6892af4ee13279a73aa58474e58a88667" uuid = "f8abcde7-e9b7-5caa-b8af-a437887ae8e4" version = "2.3.1+0" +[[deps.mpif_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "JLLWrappers", "LazyArtifacts", "Libdl", "MPIABI_jll", "MPICH_jll", "MPIPreferences", "MPItrampoline_jll", "MicrosoftMPI_jll", "OpenMPI_jll", "TOML"] +git-tree-sha1 = "a8083ee0737c243c8f40a4ba86a0956997facb73" +uuid = "9aeb927a-4695-514f-a259-621a69f20ec0" +version = "0.1.7+0" + [[deps.nghttp2_jll]] deps = ["Artifacts", "Libdl"] uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" @@ -1892,6 +2030,12 @@ git-tree-sha1 = "f349584316617063160a947a82638f7611a8ef0f" uuid = "4d7b5844-a134-5dcd-ac86-c8f19cd51bed" version = "0.41.3+0" +[[deps.s2n_tls_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "64ae051c6f03044eb7d98027d1b552b4e21e650c" +uuid = "cddc5d3d-934d-5d3a-9747-62fc12ea3f48" +version = "1.7.3+0" + [[deps.x264_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] git-tree-sha1 = "14cc7083fc6dff3cc44f2bc435ee96d06ed79aa7" diff --git a/Project.toml b/Project.toml index 582fdbf..3326a37 100644 --- a/Project.toml +++ b/Project.toml @@ -1,30 +1,36 @@ name = "TranscranialFUS" uuid = "a2de8f33-df8e-4198-bbcc-8a183cd2f9f0" -authors = ["Valentin Magis, Codex, Claude"] version = "0.1.0" +authors = ["Valentin Magis, Codex, Claude"] [deps] +CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" CondaPkg = "992eb4ea-22a4-4c89-a5bb-47a3300528ab" -CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba" DICOM = "a26e6606-dd52-5f6a-a97f-4f611373d757" +Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" +HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f" Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" +Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" [compat] +CUDA = "5" CairoMakie = "0.15" CondaPkg = "0.2" -CUDA = "5" DICOM = "0.11" +Dates = "1" FFTW = "1" +HDF5 = "0.17.2" Interpolations = "0.16" JLD2 = "0.6" JSON3 = "1" +Printf = "1" PythonCall = "0.9" Random = "1" Statistics = "1" diff --git a/README.md b/README.md index e24ce4f..026aab9 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,30 @@ -## TranscranialFUS +[![Tests](https://github.com/MagisV/hasa-pam/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/MagisV/hasa-pam/actions/workflows/tests.yml) -This repository contains the Spring 2026 project for the ETH Zurich course [Solving PDEs in parallel on GPUs with Julia II](https://pde-on-gpu.vaw.ethz.ch/part2/) (`ETHZ 101-0250-01`). +# TranscranialFUS -The project builds on earlier Python code for 2D transcranial ultrasound focusing based on the heterogeneous angular spectrum method (HASA) of Schoen and Arvanitis. The first goal was to port that focusing workflow to Julia while preserving the original end-to-end notebook pipeline. The goal of this project is to extend that foundation toward a GPU-accelerated passive acoustic mapping (PAM) workflow in Julia. +Julia project for transcranial ultrasound focusing and passive acoustic mapping (PAM), developed for the ETH Zurich course "Solving PDEs in parallel on GPUs with Julia II". -### Relation to the papers +The repository is meant to be used primarily from the command line. The maintained PAM entry point is `scripts/run_pam.jl`; focusing scripts are kept for the earlier transcranial focusing workflow. -The two main papers play different roles in this repository. +## Documentation -* **Schoen and Arvanitis (2020)** provides the computational foundation: a heterogeneous angular spectrum formulation for trans-skull aberration correction and passive acoustic mapping/source localization. That is the direct methodological basis for the focusing code and for the PAM reconstruction implemented here. -* **Ozdas et al. (2020)** provides the therapeutic motivation: focused ultrasound can be used to localize drug delivery in the brain by aggregating and uncaging ultrasound-controlled carriers with millimeter precision while keeping the blood-brain barrier intact. This repository does **not** reproduce that biological protocol directly, but it is motivated by the same broader setting of transcranial targeting and monitoring. +The detailed user documentation lives in `docs/` and is built with Documenter.jl. -### Current scope - -The repository currently has two main components: - -* **Focusing**: a Julia implementation of the transcranial focusing workflow, kept close to the structure of a Python notebook implemented for a research project within the Neurotechnology Group at ETH. -* **PAM**: a 2D passive acoustic mapping pipeline in Julia, including geometric ASA and heterogeneous ASA-based reconstruction - -At the moment, the codebase includes Julia-native CT loading and preprocessing, medium construction for transcranial simulations and focusing setup (which are all part of the previous project and provide the relevant scaffolding), a thin Python bridge to `k-wave-python` for forward simulations, and Julia implementations of the passive mapping pipeline, including scripts for single cases, estimator comparisons, and parameter sweeps. - -### Why this project - -A central challenge in transcranial ultrasound is that the skull distorts both transmitted and received wavefields. For therapy, this means the transmit field must be corrected so that acoustic energy reaches the intended intracranial target. For monitoring, signals emitted inside the brain must be mapped back through the skull so that their source locations can be estimated accurately. Schoen and Arvanitis address both problems with a computationally efficient heterogeneous ASA model for trans-skull focusing and passive source localization. - -In parallel, Ozdas et al. shows why this matters in a biomedical setting: focused ultrasound can be used to spatially target drug carriers, first by aggregating them and then by uncaging their payload locally, enabling focal delivery with much lower systemic dose and without detectable BBB opening. That paper does not implement passive acoustic mapping, but it motivates the need for reliable transcranial focusing and, more broadly, for methods that could support localization and monitoring of acoustically active agents in the brain. - -### Computational idea - -The focusing workflow follows the standard aberration-correction idea used in simulation-based transcranial focusing. A virtual source is placed at the desired target, and its field is propagated outward through a CT-derived heterogeneous skull model to the transducer plane. The phase, and optionally amplitude, predicted at the array are then conjugated and used as the transmit drive. In this repository, that correction step is modeled with HASA, while `k-wave-python` is used as the higher-fidelity forward model for the emitted field. - -The PAM workflow uses the complementary problem. Here the sources are real emitters inside the brain, or point sources in a forward simulation. Their signals propagate outward to the array, producing RF data. Reconstruction then back-propagates those measurements through the same heterogeneous medium model to estimate the source location. In the language of Schoen and Arvanitis, this is passive acoustic mapping or point-source localization through the skull. +- Start with `docs/src/getting-started.md`. +- Use `docs/src/cli/run-pam.md` for PAM examples. +- Use `docs/src/cli/parameters.md` for the generated PAM parameter reference after building the docs. +- Use `docs/src/workflow.md` for the high-level source, medium, simulation, reconstruction, and analysis flow. +Build the documentation locally with: +```bash +julia --project=docs docs/make.jl +``` -## Repository Layout +### Hosted docs (GitHub Pages) -- `src/ct.jl`: DICOM loading, ROI crop, and XY resampling -- `src/medium.jl`: HU conversion, skull masking, and focusing-medium construction -- `src/focus.jl`: focusing configs, placement handling, HASA/geometric delays, and plotting helpers -- `src/pam.jl`: compatibility include for the split PAM implementation under `src/pam/` -- `src/pam/sources.jl`: PAM source models, squiggle source construction, source signals, and phase variability -- `src/pam/config.jl`: PAM configs, grid helpers, fitting, and source indexing -- `src/pam/medium.jl`: skull/lens PAM medium generation -- `src/pam/reconstruction.jl`: geometric ASA/HASA propagation and windowed reconstruction loop -- `src/pam/analysis.jl`: PAM peaks, masks, PSF helpers, localization metrics, and detection metrics -- `src/pam/workflow.jl`: case-level simulation/reconstruction orchestration -- `src/pam/sweep.jl`: PAM sweep helpers and aggregation -- `src/kwave_wrapper.jl`: Julia-to-`k-wave-python` bridge -- `src/analysis.jl`: focusing analysis helpers and `run_focus_case` -- `scripts/run_focus_case.jl`: run one focusing case -- `scripts/compare_estimators.jl`: compare geometric and HASA focusing -- `scripts/run_pam.jl`: unified PAM runner for coordinate-placed point sources and squiggle activity -- `scripts/run_pam_sweep.jl`: run a single-source localization sweep and summarize corrected vs uncorrected error -- `test/runtests.jl`: unit tests, smoke tests, and optional integration tests +The site is built and deployed automatically when changes land on `main` +([Deploy Documentation](https://github.com/MagisV/hasa-pam/actions/workflows/deploy-docs.yml)). ## Setup @@ -62,204 +33,30 @@ Instantiate the Julia environment: ```bash julia --project=. -e 'using Pkg; Pkg.instantiate()' ``` +` +Python-side dependencies for k-Wave are managed through `CondaPkg.toml`. The first k-Wave run may resolve Python packages and k-Wave resources. -Python-side dependencies are managed through [CondaPkg.toml](/Users/vm/INI_code/Julia%20II/CondaPkg.toml). The current forward-model backend uses: - -- Python `3.11` to `3.12` -- `k-wave-python==0.4.0` -- `numpy` -- `scipy` - -The first call into the k-Wave wrapper may need to resolve Python packages and k-Wave resources. - -## CT Data - -The default CT path points to the local DICOM folder: - -`C:\Users\AU-FUS-Valentin\Desktop\OBJ_0001` - -You can override it in the scripts with `--ct-path=...`. - -The CT scan data used in this project is private and is not distributed with the repository. If you need access to the dataset, please reach out to the author. - -The loader is designed to mirror the working notebook behavior: - -- select the dominant DICOM series in the folder -- crop the ROI using the notebook defaults - - `index_xyz = (170, 190, 400)` - - `size_xyz = (705, 360, 450)` -- resample `x` and `y` to `0.20 mm` -- keep the original `z` spacing - -## Focusing Workflow - -The focusing path keeps the original notebook-style naming and behavior where practical: - -- `load_roi_resample_xy` -- `hu_to_rho_c` -- `find_skull_boundaries` -- `skull_mask_from_c_columnwise` -- `make_medium` -- `focus` -- `analyse_focus_2d` - -Supported focusing media: - -- `water` -- `skull_in_water` +## Data -Supported estimators: - -- `geometric` -- `hasa` - -### Placement Modes - -The focusing scripts support the two placement mechanisms from the original pipeline: - -- `--placement=fixed_transducer` - Skull placement and focal target placement are both defined relative to the transducer. `--focal-cm` is the transducer-to-focus distance. -- `--placement=fixed_focus_depth` - The target is placed at `--focus-depth-from-inner-skull-mm` below the inner skull, and `--focal-cm` determines how far away the transducer is from that target. -- `--placement=auto` - For skull runs this defaults to `fixed_focus_depth` with `30 mm` below the inner skull. For water runs it defaults to `fixed_transducer`. - -### Run One Focusing Case - -Default skull example: - -```bash -julia --project=. scripts/run_focus_case.jl \ - --estimator=hasa \ - --medium=skull_in_water \ - --slice-index=250 -``` - -Centered target at `60 mm` below the transducer: +The CT scan data used for skull-backed examples is private and is not distributed with the repository. Homogeneous water runs with `--aberrator=none` do not need CT data. For skull-backed runs, pass: ```bash -julia --project=. scripts/run_focus_case.jl \ - --estimator=hasa \ - --medium=skull_in_water \ - --placement=fixed_transducer \ - --slice-index=250 \ - --lateral-cm=0.0 \ - --focal-cm=6.0 +--ct-path=/path/to/dicom-folder ``` -Target shifted `10 mm` to the right: - -```bash -julia --project=. scripts/run_focus_case.jl \ - --estimator=hasa \ - --medium=skull_in_water \ - --placement=fixed_transducer \ - --slice-index=250 \ - --lateral-cm=1.0 \ - --focal-cm=6.0 -``` +## Quick PAM Example -Target `30 mm` below the inner skull: - -```bash -julia --project=. scripts/run_focus_case.jl \ - --estimator=hasa \ - --medium=skull_in_water \ - --placement=fixed_focus_depth \ - --slice-index=250 \ - --lateral-cm=0.0 \ - --focal-cm=6.0 \ - --focus-depth-from-inner-skull-mm=30 -``` - -### Compare Geometric vs HASA Focusing - -```bash -julia --project=. scripts/compare_estimators.jl \ - --medium=skull_in_water \ - --slice-index=250 -``` - -### Focusing Outputs - -`run_focus_case.jl` writes: - -- `summary.json` -- `result.jld2` -- `pressure.png` - -`compare_estimators.jl` writes: - -- `summary.json` -- `comparison.png` - -Both scripts generate an `outputs/...` directory name automatically from the main run parameters and a timestamp. `--out-dir=...` overrides that behavior. - -## Passive Acoustic Mapping - -The PAM path implements a 2D passive reconstruction workflow based on simple point emitters and denser bubble-cluster activity: - -- forward propagation simulated with `k-wave-python` -- geometric ASA reconstruction -- corrected HASA reconstruction -- localization and image-quality metrics such as axial, lateral, and radial error, FWHM, peak intensity, and success rate -- thresholded detection metrics for dense aggregate cases, including precision, recall, F1, false-positive area, and false-negative area - -Core types: - -- `PointSource2D` -- `BubbleCluster2D` -- `GaussianPulseCluster2D` -- `PAMConfig` -- `PAMWindowConfig` -- `SourceVariabilityConfig` - -Key helpers: - -- `fit_pam_config` -- `make_squiggle_bubble_sources` -- `make_pam_medium` -- `simulate_point_sources` -- `reconstruct_pam` -- `reconstruct_pam_windowed` -- `find_pam_peaks` -- `pam_centerline_truth_mask` -- `analyse_pam_2d` -- `analyse_pam_detection_2d` -- `run_pam_case` -- `run_pam_sweep` - -### PAM Medium Options - -Supported aberrators: - -- `--aberrator=none`: homogeneous water control. Corrected and uncorrected PAM should match because there is no heterogeneous phase error to correct. -- `--aberrator=lens`: simple elliptical speed perturbation -- `--aberrator=skull`: CT-derived skull inserted into the PAM domain - -For `--aberrator=skull`: - -- the receiver plane stays at the top of the physical domain -- the outer skull surface is placed `--skull-transducer-distance-mm` below the receiver, default `30 mm` -- source coordinates stay defined relative to the transducer/receiver, not relative to the skull -- `fit_pam_config` extends the axial domain automatically to fit the deepest source plus `--bottom-margin-mm` -- `fit_pam_config` also extends `t_max` when a deep or long-gated source would otherwise be truncated -- the run scripts default to `--recon-step-um=50` for HASA/ASA axial integration, matching the trans-skull PAM paper setup - -### Run One PAM Case - -The maintained PAM entrypoint is `scripts/run_pam.jl`. It supports coordinate-placed point sources and squiggle activity sources. - -Single-source homogeneous-water baseline: +Small homogeneous 2D point-source run: ```bash julia --project=. scripts/run_pam.jl \ --source-model=point \ --sources-mm=30:0 \ - --aberrator=none + --aberrator=none \ + --recon-use-gpu=false ``` -Simple 3D homogeneous-water point-source run (requires a CUDA-capable GPU): +Simple 3D point-source run on CUDA: ```bash julia --project=. scripts/run_pam.jl \ @@ -267,451 +64,18 @@ julia --project=. scripts/run_pam.jl \ --source-model=point \ --sources-mm=30:2:-1 \ --aberrator=none \ - --use-gpu=true -``` - -Multiple point emitters with explicit phase and delay control: - -```bash -julia --project=. scripts/run_pam.jl \ - --source-model=point \ - --sources-mm=25:-6,32:0,40:8 \ - --phases-deg=0,90,180 \ - --delays-us=0,2,4 \ - --aberrator=lens -``` - -Squiggle activity uses `--anchors-mm=depth:lateral,...`; each anchor expands into harmonic bubble emitters sampled along one squiggly centerline. In `--recon-mode=auto`, `--source-model=squiggle` selects windowed incoherent reconstruction: - -```text -I_total(x,z) = mean_windows sum_f |p_hasa(x,z,f,window)|^2 -``` - -Current leading squiggle setting: - -```bash -julia --project=. scripts/run_pam.jl ` - --source-model=squiggle ` - --anchors-mm=45:0 ` - --aberrator=skull ` - --boundary-threshold-ratios=0.6,0.65,0.7 ` - --cavitation-model=harmonic-cos ` - --frequency-jitter-percent=1 ` - --harmonic-amplitudes=1.0,0.6,0.3 ` - --harmonics=2,3,4 ` - --random-seed=45 ` - --receiver-aperture-mm=full ` - --recon-bandwidth-khz=500 ` - --recon-hop-us=10 ` - --recon-min-window-energy-ratio=0.001 ` - --recon-window-us=20 ` - --skull-transducer-distance-mm=30 ` - --slice-index=250 ` - --source-phase-mode=random_phase_per_window ` - --t-max-us=500 ` - --transverse-mm=102.4 ` - --vascular-length-mm=12 ` - --recon-progress=true ` - --use-gpu=true ` - --benchmark=true -``` - -Running reconstruction only with previous k-wave simulation as data: - -```bash -julia --project=. scripts/run_pam.jl ` - --from-run-dir=outputs/20260507_102449_run_pam_skull_squiggle_1anchors_21src_ax80p0mm_lat102p0mm_f0p5mhz_h234_slice250_st30p0mm_randomphaseperwindow ` - --boundary-threshold-ratios=0.6,0.65,0.7 ` - --recon-bandwidth-khz=500 ` - --recon-hop-us=10 ` - --recon-min-window-energy-ratio=0.001 ` - --recon-window-us=20 ` - --recon-progress=true ` - --use-gpu=true ` - --window-batch=8 ` - --benchmark=false -``` - -3D -```bash -julia --project=. scripts/run_pam.jl ` - --dimension=3 ` - --source-model=point ` - --sources-mm=30:2:-1 ` - --aberrator=none ` - --use-gpu=true ` -``` - -```bash -julia --project=. scripts/run_pam.jl ` - --dimension=3 ` - --source-model=point ` - --sources-mm=30:2:-1 ` - --frequency-mhz=0.5 ` - --num-cycles=5 ` - --aberrator=none ` - --axial-mm=60 ` - --transverse-mm=32 ` - --t-max-us=60 ` - --receiver-aperture-mm=full ` - --use-gpu=true ` - --recon-progress=true - -``` - -3D squiggle vascular source (homogeneous water): - -```bash -julia --project=. scripts/run_pam.jl ` - --dimension=3 ` - --source-model=squiggle ` - --anchors-mm=55:0:0 ` - --vascular-length-mm=12 ` - --vascular-squiggle-amplitude-mm=1.5 ` - --vascular-squiggle-amplitude-x-mm=1.0 ` - --squiggle-phase-x-deg=90 ` - --harmonics=2,3,4 ` - --harmonic-amplitudes=1.0,0.6,0.3 ` - --aberrator=none ` - --axial-mm=80 ` - --transverse-mm=64 ` - --dx-mm=0.2 ` - --dy-mm=0.5 ` - --dz-mm=0.5 ` - --t-max-us=250 ` - --sim-mode=kwave ` - --use-gpu=true ` - --recon-progress=true -``` - -3D squiggle vascular source with CT skull: - -```bash -julia --project=. scripts/run_pam.jl ` - --dimension=3 ` - --source-model=squiggle ` - --gate-us=45 ` - --anchors-mm=42:0:0 ` - --vascular-length-mm=12 ` - --vascular-squiggle-amplitude-mm=0.3 ` - --vascular-squiggle-amplitude-x-mm=0.2 ` - --vascular-squiggle-wavelength-mm=8 ` - --squiggle-phase-x-deg=90 ` - --vascular-source-spacing-mm=0.5 ` - --vascular-min-separation-mm=0.25 ` - --harmonics=2,3,4 ` - --harmonic-amplitudes=1.0,0.6,0.3 ` - --aberrator=skull ` - --skull-transducer-distance-mm=20 ` - --slice-index=250 ` - --axial-mm=70 ` - --transverse-mm=64 ` - --dx-mm=0.2 ` - --dy-mm=0.5 ` - --dz-mm=0.5 ` - --t-max-us=250 ` - --fundamental-mhz=0.5 ` - --receiver-aperture-mm=full ` - --source-phase-mode=random_phase_per_window ` - --frequency-jitter-percent=1 ` - --recon-window-us=40 ` - --recon-hop-us=20 ` - --recon-bandwidth-khz=40 ` - --auto-threshold-search=true ` - --auto-threshold-min=0.10 ` - --auto-threshold-max=0.95 ` - --auto-threshold-step=0.01 ` - --sim-mode=kwave ` - --use-gpu=true ` - --window-batch=2 ` - --recon-progress=true + --recon-use-gpu=true ``` -3D synthetic vascular network at the transducer focus: +## Quick Focusing Example ```bash -julia --project=. scripts/run_pam.jl ` - --dimension=3 ` - --source-model=network ` - --anchors-mm=45:0:0 ` - --network-axial-radius-mm=10 ` - --network-lateral-y-radius-mm=1.5 ` - --network-lateral-z-radius-mm=1.5 ` - --network-root-count=12 ` - --network-generations=3 ` - --network-branch-length-mm=5 ` - --network-branch-step-mm=0.4 ` - --network-branch-angle-deg=36 ` - --network-tortuosity=0.18 ` - --network-orientation=isotropic ` - --network-density-axial-sigma-mm=10.0 ` - --network-density-lateral-y-sigma-mm=1.5 ` - --network-density-lateral-z-sigma-mm=1.5 ` - --network-max-sources-per-center=80 ` - --vascular-source-spacing-mm=0.5 ` - --vascular-min-separation-mm=0.25 ` - --fundamental-mhz=0.5 ` - --harmonics=2,3,4 ` - --harmonic-amplitudes=1.0,0.6,0.3 ` - --aberrator=skull ` - --skull-transducer-distance-mm=20 ` - --slice-index=250 ` - --axial-mm=70 ` - --transverse-mm=64 ` - --dx-mm=0.2 ` - --dy-mm=0.5 ` - --dz-mm=0.5 ` - --t-max-us=250 ` - --receiver-aperture-mm=full ` - --source-phase-mode=random_phase_per_window ` - --frequency-jitter-percent=1 ` - --recon-window-us=40 ` - --recon-hop-us=20 ` - --recon-bandwidth-khz=40 ` - --auto-threshold-search=true ` - --auto-threshold-min=0.10 ` - --auto-threshold-max=0.95 ` - --auto-threshold-step=0.01 ` - --sim-mode=kwave ` - --use-gpu=true ` - --window-batch=2 ` - --recon-progress=true -``` - -`--source-model=network` grows a random branching 3D centerline structure, clips -it to an ellipsoid around each `--anchors-mm=depth:y:z` center, then samples -bubble emitters along those branches with an anisotropic Gaussian density. The -default 500 kHz focal-volume support is roughly `20 mm` axial length by `3 mm` -lateral width (`10 mm` axial radius and `1.5 mm` Y/Z radii). The default density -sigmas match those radii, and the source cap is `80` bubbles per center. -`--network-orientation=horizontal` and `--network-orientation=axial` are -available for controlled priors, and `--network-radius-mm` remains available as -a spherical shorthand. - -For sparse 3D squiggle and network sources, the threshold summary reports both voxel overlap -metrics and source-aware metrics. `source_f1` combines voxel precision with the -fraction of simulated bubble centers hit within the source detection radius, and -is used to select the best 3D threshold. By default, 3D analysis runs a dense -post-reconstruction threshold search and `activity_boundaries.png` shows the -precision, recall, and F1 curves plus three readable outlines: best F1, a -recall-biased threshold, and a precision-biased threshold. - -The reconstruction bandwidth is an important runtime knob: tighter bandwidths -select fewer FFT frequency bins for ASA/HASA, reducing reconstruction time -roughly in proportion to the frequency-bin count. In the focused 3D skull -sweeps, `40 kHz` bandwidth with `1%` frequency jitter was the best runtime -default: it stayed in the same F1 range as wider bands while reducing the HASA -march time substantially. `60 kHz` with `2%` jitter recovered a little more F1 -at extra cost and is a useful accuracy check. - -3D heterogeneous skull medium (CT-backed): - -```bash -julia --project=. scripts/run_pam.jl ` - --dimension=3 ` - --transverse-mm=64 ` - --axial-mm=80 ` - --dx-mm=0.2 ` - --dy-mm=0.5 ` - --dz-mm=0.5 ` - --t-max-us=80 ` - --source-model=point ` - --sources-mm=50:2:-1 ` - --num-cycles=5 ` - --aberrator=skull ` - --skull-transducer-distance-mm=30 ` - --frequency-mhz=0.5 ` - --slice-index=250 ` - --receiver-aperture-mm=full ` - --use-gpu=true ` - --recon-progress=true -``` - -The PAM run scripts write: - -- `overview.png` -- `activity_boundaries.png`, with threshold-dependent active-region boundaries overlaid on the heatmaps, precision/recall/F1 threshold curves, and selected-threshold metrics -- `summary.json` -- `result.jld2` - -To rerun only reconstruction and figure generation from saved RF data, pass an existing output folder: - -```bash -julia --project=. scripts/run_pam.jl \ - --from-run-dir=outputs/previous_pam_run \ - --recon-bandwidth-khz=20 -``` - -`--from-run-dir` loads the previous `result.jld2`, reuses its RF data, medium, grid, and sources, and writes a fresh `outputs/_reconstruct_/` directory. Simulation-specific options such as source locations, medium/skull settings, grid size, and time step are rejected in this mode; reconstruction and analysis options such as `--use-gpu`, `--recon-bandwidth-khz`, `--recon-step-um`, `--recon-frequencies-mhz`, `--peak-method`, and detection thresholds remain adjustable. - -### CUDA PAM Reconstruction - -`--use-gpu=true` enables the CUDA.jl PAM reconstruction backend. It requires a functional NVIDIA CUDA GPU; if CUDA.jl cannot see one, reconstruction errors clearly instead of silently falling back to CPU. The first CUDA path uses Float32 device arithmetic, keeps the existing shifted FFT convention (`fftshift`/`ifftshift`) for CPU/GPU parity, and still marches corrected HASA rows serially while running the lateral FFTs and per-row vector operations on the GPU. - -Small case for fast CUDA iteration (no CT data required, ~10 s end-to-end): - -```bash -julia --project=. scripts/run_pam.jl ` - --source-model=point ` - --sources-mm=30:0 ` - --aberrator=none ` - --axial-mm=40 ` - --transverse-mm=51.2 ` - --t-max-us=100 ` - --use-gpu=true ` - --recon-progress=true -``` - -### Source Phase Modes - -`--source-phase-mode` controls the physical regime being simulated and is reported in `summary.json`. - -| Mode | Physical meaning | -|---|---| -| `coherent` | All sources share the same phase relation. Contributions add constructively/destructively by geometry. | -| `random_static_phase` | Each source draws a random phase once at setup and keeps it for the full simulation. | -| `random_phase_per_window` | Each source emits once per reconstruction window with fresh random phases. A **single** k-Wave simulation spans all windows; windowed reconstruction is forced automatically. | -| `random_phase_per_realization` | Each of `--n-realizations` k-Wave runs draws fresh random phases; intensity maps are averaged across runs. | - -**Coherent baseline** — sources lock in phase, single simulation: - -```bash -julia --project=. scripts/run_pam.jl \ - --source-model=point \ - --sources-mm=30:0 \ - --source-phase-mode=coherent \ - --phase-mode=coherent \ - --aberrator=none -``` - -**Random static phase** — fixed random phases, single simulation: - -```bash -julia --project=. scripts/run_pam.jl \ - --source-model=squiggle \ - --anchors-mm=30:0 \ - --vascular-length-mm=12 \ - --source-phase-mode=random_static_phase \ - --phase-mode=random \ - --random-seed=42 \ - --aberrator=none -``` - -**Incoherent averaging over realizations** — 20 independent phase draws: - -```bash -julia --project=. scripts/run_pam.jl \ - --source-model=squiggle \ - --anchors-mm=30:0 \ - --vascular-length-mm=12 \ - --source-phase-mode=random_phase_per_realization \ - --n-realizations=20 \ - --random-seed=42 \ - --aberrator=none -``` - -**Incoherent averaging per window** — single k-Wave run; each source gets fresh random phases per window: - -```bash -julia --project=. scripts/run_pam.jl \ - --source-model=squiggle \ - --anchors-mm=30:0 \ - --vascular-length-mm=12 \ - --source-phase-mode=random_phase_per_window \ - --recon-window-us=10 \ - --recon-hop-us=5 \ - --random-seed=42 \ - --aberrator=none -``` - -For per-window random phase, `--frequency-jitter-percent` applies a multiplicative jitter to each source fundamental frequency. - -```bash -julia --project=. scripts/run_pam.jl \ - --source-model=squiggle \ - --anchors-mm=30:0 \ - --vascular-length-mm=12 \ - --source-phase-mode=random_phase_per_window \ - --recon-window-us=10 \ - --recon-hop-us=5 \ - --frequency-jitter-percent=5 \ - --t-max-us=200 \ - --random-seed=42 \ - --aberrator=none -``` - -Supported amplitude distributions are `fixed`, `uniform`, `lognormal`, and `gaussian`. For `uniform`, `--amplitude-sigma` is the relative half-width around each source amplitude. For `lognormal`, it is the log-space standard deviation. For `gaussian`, it is the relative standard deviation and sampled amplitudes are clipped at zero. `--frequency-jitter-percent` applies a multiplicative jitter to each source fundamental frequency, so harmonic frequencies shift with it. The selected settings are written to `summary.json` under `source_variability`. - -**Stochastic broadband** — each source emits independent noise centred on its harmonic frequencies: - -```bash -julia --project=. scripts/run_pam.jl \ - --source-model=squiggle \ - --anchors-mm=30:0 \ - --vascular-length-mm=12 \ - --source-phase-mode=random_phase_per_window \ - --random-seed=42 \ - --aberrator=none -``` - -### Run a PAM Sweep - -The sweep script runs **single-source** reconstructions over a target grid and compares uncorrected vs corrected localization. - -Default paper-style sweep: - -```bash -julia --project=. scripts/run_pam_sweep.jl -``` - -This defaults to: - -- `--aberrator=skull` -- `--frequency-mhz=1.0` -- `--receiver-aperture-mm=50` -- axial targets `30,40,50,60,70,80 mm` -- lateral targets `-20,-10,0,10,20 mm` -- `--slice-index=250` -- `--skull-transducer-distance-mm=30` - -Quick sweep for fast figure generation: - -```bash -julia --project=. scripts/run_pam_sweep.jl --sweep-preset=quick -``` - -The quick preset uses: - -- axial targets `40,60,80 mm` -- lateral targets `-10,0,10 mm` - -Custom sweep: - -```bash -julia --project=. scripts/run_pam_sweep.jl \ - --sweep-preset=custom \ - --axial-targets-mm=40,50,60 \ - --lateral-targets-mm=-10,0,10 \ - --aberrator=skull -``` - -Custom example cases for the overview panel: - -```bash -julia --project=. scripts/run_pam_sweep.jl \ - --sweep-preset=quick \ - --example-targets-mm=40:0,60:0,80:10 +julia --project=. scripts/run_focus.jl \ + --estimator=hasa \ + --medium=skull_in_water \ + --slice-index=250 ``` -For skull sweeps, requested targets are filtered so that only points inside the cranial cavity are retained. The margin from the inner skull can be adjusted with `--skull-cavity-margin-mm`. - -`run_pam_sweep.jl` writes: - -- `overview.png` -- `summary.json` -- `result.jld2` -- `cases/`: one figure per retained target, each showing uncorrected vs corrected reconstruction - ## Tests Run the standard test suite: @@ -720,29 +84,18 @@ Run the standard test suite: julia --project=. -e 'using Pkg; Pkg.test()' ``` -Enable the k-Wave smoke tests: +Enable optional k-Wave smoke tests: ```bash TRANSCRANIALFUS_RUN_KWAVE_TESTS=1 julia --project=. -e 'using Pkg; Pkg.test()' ``` -Enable the heavier CT-backed integration test as well: +Enable the heavier CT-backed integration tests as well: ```bash TRANSCRANIALFUS_RUN_INTEGRATION=1 TRANSCRANIALFUS_RUN_KWAVE_TESTS=1 julia --project=. -e 'using Pkg; Pkg.test()' ``` -The tests currently cover: - -- HU-to-medium conversion -- skull boundary detection and masking -- focusing placement resolution -- squiggle PAM source generation and detection metrics -- PAM medium fitting and skull placement -- PAM sweep aggregation and target filtering -- source phase mode normalisation and per-window phase/frequency resampling -- opt-in k-Wave smoke tests - ## AI Usage -This project was developed with AI assistance. OpenAI Codex was used for code generation, debugging, refactoring, testing support, and documentation updates. All generated code and text remain the responsibility of the project authors and should be reviewed critically before use. +This project was developed with AI assistance. Generated code and text should be reviewed critically before use. diff --git a/coverage.jl b/coverage.jl new file mode 100644 index 0000000..8662d86 --- /dev/null +++ b/coverage.jl @@ -0,0 +1,109 @@ +#!/usr/bin/env julia + +# Generate LCOV output from Julia .cov files produced by `Pkg.test(; coverage=true)`. +import Pkg + +const ROOT = @__DIR__ +const COVERAGE_DIR = joinpath(ROOT, "coverage") +const LCOV_PATH = joinpath(COVERAGE_DIR, "lcov.info") +const SUMMARY_PATH = joinpath(COVERAGE_DIR, "summary.txt") + +Pkg.activate(; temp=true) +Pkg.add("Coverage") + +using Coverage + +const EXCLUDED_FILES = Set( + normpath.([ + joinpath(ROOT, "src", "common", "kwave_wrapper.jl"), + joinpath(ROOT, "scripts", "run_pam.jl"), + ]), +) + +const EXCLUDED_BLOCK_STARTS = Dict( + normpath(joinpath(ROOT, "src", "pam", "2d", "reconstruction.jl")) => [ + r"^const _PAM_CUDA_", + r"^function _pam_cuda_functional\(", + r"^function _assert_pam_cuda_available\(", + r"^function _accum_abs2_sum", + r"^struct PAMCUDASetup\b", + r"^function _pam_cuda_setup\(", + r"^function _reconstruct_pam_cuda\(", + ], + normpath(joinpath(ROOT, "src", "pam", "3d", "reconstruction3d.jl")) => [ + r"^struct PAMCUDASetup3D\b", + r"^function _pam_cuda_setup_3d\(", + r"^function _accum_abs2_sum_batched_3d!\(", + r"^function _reconstruct_pam_cuda_3d\(", + r"^function reconstruct_pam_windowed_3d\(", + ], +) + +function _block_end(source, start_idx::Int) + lines = split(source, '\n'; keepempty=true) + line = lines[start_idx] + starts_block = occursin(r"^(function|struct)\b", line) + starts_block || return start_idx + + depth = 0 + for idx in start_idx:length(lines) + stripped = strip(lines[idx]) + if occursin(r"^(function|struct|if|for|while|let|begin|try|macro)\b", stripped) + depth += 1 + end + if stripped == "end" + depth -= 1 + depth == 0 && return idx + end + end + return length(source) +end + +function _exclude_block_coverage!(fc::Coverage.FileCoverage) + path = normpath(fc.filename) + starts = get(EXCLUDED_BLOCK_STARTS, path, nothing) + isnothing(starts) && return fc + + lines = split(fc.source, '\n'; keepempty=true) + idx = 1 + while idx <= min(length(lines), length(fc.coverage)) + line = lines[idx] + if any(pattern -> occursin(pattern, line), starts) + stop = _block_end(fc.source, idx) + stop = min(stop, length(fc.coverage)) + fc.coverage[idx:stop] .= nothing + idx = stop + 1 + else + idx += 1 + end + end + return fc +end + +function _apply_coverage_policy!(coverage) + filter!(fc -> normpath(fc.filename) ∉ EXCLUDED_FILES, coverage) + foreach(_exclude_block_coverage!, coverage) + return coverage +end + +mkpath(COVERAGE_DIR) + +coverage = Coverage.FileCoverage[] +append!(coverage, process_folder(joinpath(ROOT, "src", "pam"))) +append!(coverage, process_folder(joinpath(ROOT, "src", "common"))) +push!(coverage, process_file(joinpath(ROOT, "scripts", "run_pam.jl"))) # filtered: covered by src/pam/setup/runner.jl +_apply_coverage_policy!(coverage) + +covered_lines, total_lines = get_summary(coverage) +coverage_percent = total_lines == 0 ? 100.0 : 100.0 * covered_lines / total_lines + +LCOV.writefile(LCOV_PATH, coverage) + +open(SUMMARY_PATH, "w") do io + println(io, "covered_lines=", covered_lines) + println(io, "total_lines=", total_lines) + println(io, "coverage_percent=", round(coverage_percent; digits=2)) +end + +println("Coverage: $(round(coverage_percent; digits=2))% ($(covered_lines)/$(total_lines) lines)") +println("Wrote $(relpath(LCOV_PATH, ROOT))") diff --git a/docs/Manifest.toml b/docs/Manifest.toml new file mode 100644 index 0000000..1704024 --- /dev/null +++ b/docs/Manifest.toml @@ -0,0 +1,324 @@ +# This file is machine-generated - editing it directly is not advised + +julia_version = "1.12.2" +manifest_format = "2.0" +project_hash = "25eeb04ae1077211d0ddfd42583ddf2c96f33bc0" + +[[deps.ANSIColoredPrinters]] +git-tree-sha1 = "574baf8110975760d391c710b6341da1afa48d8c" +uuid = "a4c015fc-c6ff-483c-b24f-f7ea428134e9" +version = "0.0.1" + +[[deps.AbstractTrees]] +git-tree-sha1 = "2d9c9a55f9c93e8887ad391fbae72f8ef55e1177" +uuid = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" +version = "0.4.5" + +[[deps.ArgTools]] +uuid = "0dad84c5-d112-42e6-8d28-ef12dabb789f" +version = "1.1.2" + +[[deps.Artifacts]] +uuid = "56f22d72-fd6d-98f1-02f0-08ddc0907c33" +version = "1.11.0" + +[[deps.Base64]] +uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" +version = "1.11.0" + +[[deps.CodecZlib]] +deps = ["TranscodingStreams", "Zlib_jll"] +git-tree-sha1 = "962834c22b66e32aa10f7611c08c8ca4e20749a9" +uuid = "944b1d66-785c-5afd-91f1-9de20f533193" +version = "0.7.8" + +[[deps.CompilerSupportLibraries_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" +version = "1.3.0+1" + +[[deps.Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" +version = "1.11.0" + +[[deps.DocStringExtensions]] +git-tree-sha1 = "7442a5dfe1ebb773c29cc2962a8980f47221d76c" +uuid = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +version = "0.9.5" + +[[deps.Documenter]] +deps = ["ANSIColoredPrinters", "AbstractTrees", "Base64", "CodecZlib", "Dates", "DocStringExtensions", "Downloads", "Git", "IOCapture", "InteractiveUtils", "JSON", "Logging", "Markdown", "MarkdownAST", "Pkg", "PrecompileTools", "REPL", "RegistryInstances", "SHA", "TOML", "Test", "Unicode"] +git-tree-sha1 = "56e9c37b5e7c3b4f080ab1da18d72d5c290e184a" +uuid = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +version = "1.17.0" + +[[deps.Downloads]] +deps = ["ArgTools", "FileWatching", "LibCURL", "NetworkOptions"] +uuid = "f43a241f-c20a-4ad4-852c-f6b1247861c6" +version = "1.7.0" + +[[deps.Expat_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "8f05e9a2e7c2e3eb524102bb2926c5743c07fbe1" +uuid = "2e619515-83b5-522b-bb60-26c02a35a201" +version = "2.8.0+0" + +[[deps.FileWatching]] +uuid = "7b1f6079-737a-58dc-b8bc-7a2ca5c1b5ee" +version = "1.11.0" + +[[deps.Git]] +deps = ["Git_LFS_jll", "Git_jll", "JLLWrappers", "OpenSSH_jll"] +git-tree-sha1 = "824a1890086880696fc908fe12a17bcf61738bd8" +uuid = "d7ba0133-e1db-5d97-8f8c-041e4b3a1eb2" +version = "1.5.0" + +[[deps.Git_LFS_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "bb8471f313ed941f299aa53d32a94ab3bee08844" +uuid = "020c3dae-16b3-5ae5-87b3-4cb189e250b2" +version = "3.7.0+0" + +[[deps.Git_jll]] +deps = ["Artifacts", "Expat_jll", "JLLWrappers", "LibCURL_jll", "Libdl", "Libiconv_jll", "OpenSSL_jll", "PCRE2_jll", "Zlib_jll"] +git-tree-sha1 = "0dd4cfb426924210c8f42742751cbde74b27bfa3" +uuid = "f8c6e375-362e-5223-8a59-34ff63f689eb" +version = "2.54.0+0" + +[[deps.IOCapture]] +deps = ["Logging", "Random"] +git-tree-sha1 = "0ee181ec08df7d7c911901ea38baf16f755114dc" +uuid = "b5f81e59-6552-4d32-b1f0-c071b021bf89" +version = "1.0.0" + +[[deps.InteractiveUtils]] +deps = ["Markdown"] +uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" +version = "1.11.0" + +[[deps.JLLWrappers]] +deps = ["Artifacts", "Preferences"] +git-tree-sha1 = "7204148362dafe5fe6a273f855b8ccbe4df8173e" +uuid = "692b3bcd-3c85-4b1f-b108-f13ce0eb3210" +version = "1.8.0" + +[[deps.JSON]] +deps = ["Dates", "Logging", "Parsers", "PrecompileTools", "StructUtils", "UUIDs", "Unicode"] +git-tree-sha1 = "fe23330af47b8ab4e135b2ff65f7398c3a2bfc65" +uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +version = "1.5.2" + + [deps.JSON.extensions] + JSONArrowExt = ["ArrowTypes"] + + [deps.JSON.weakdeps] + ArrowTypes = "31f734f8-188a-4ce0-8406-c8a06bd891cd" + +[[deps.JuliaSyntaxHighlighting]] +deps = ["StyledStrings"] +uuid = "ac6e5ff7-fb65-4e79-a425-ec3bc9c03011" +version = "1.12.0" + +[[deps.LazilyInitializedFields]] +git-tree-sha1 = "0f2da712350b020bc3957f269c9caad516383ee0" +uuid = "0e77f7df-68c5-4e49-93ce-4cd80f5598bf" +version = "1.3.0" + +[[deps.LibCURL]] +deps = ["LibCURL_jll", "MozillaCACerts_jll"] +uuid = "b27032c2-a3e7-50c8-80cd-2d36dbcbfd21" +version = "0.6.4" + +[[deps.LibCURL_jll]] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll", "Zlib_jll", "nghttp2_jll"] +uuid = "deac9b47-8bc7-5906-a0fe-35ac56dc84c0" +version = "8.15.0+0" + +[[deps.LibGit2]] +deps = ["LibGit2_jll", "NetworkOptions", "Printf", "SHA"] +uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" +version = "1.11.0" + +[[deps.LibGit2_jll]] +deps = ["Artifacts", "LibSSH2_jll", "Libdl", "OpenSSL_jll"] +uuid = "e37daf67-58a4-590a-8e99-b0245dd2ffc5" +version = "1.9.0+0" + +[[deps.LibSSH2_jll]] +deps = ["Artifacts", "Libdl", "OpenSSL_jll"] +uuid = "29816b5a-b9ab-546f-933c-edad1886dfa8" +version = "1.11.3+1" + +[[deps.Libdl]] +uuid = "8f399da3-3557-5675-b5ff-fb832c97cbdb" +version = "1.11.0" + +[[deps.Libiconv_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl"] +git-tree-sha1 = "be484f5c92fad0bd8acfef35fe017900b0b73809" +uuid = "94ce4f54-9a6c-5748-9c1c-f9c7231a4531" +version = "1.18.0+0" + +[[deps.Logging]] +uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" +version = "1.11.0" + +[[deps.Markdown]] +deps = ["Base64", "JuliaSyntaxHighlighting", "StyledStrings"] +uuid = "d6f4376e-aef5-505a-96c1-9c027394607a" +version = "1.11.0" + +[[deps.MarkdownAST]] +deps = ["AbstractTrees", "Markdown"] +git-tree-sha1 = "93c718d892e73931841089cdc0e982d6dd9cc87b" +uuid = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" +version = "0.1.3" + +[[deps.MozillaCACerts_jll]] +uuid = "14a3606d-f60d-562e-9121-12d972cd8159" +version = "2025.5.20" + +[[deps.NetworkOptions]] +uuid = "ca575930-c2e3-43a9-ace4-1e988b2c1908" +version = "1.3.0" + +[[deps.OpenSSH_jll]] +deps = ["Artifacts", "JLLWrappers", "Libdl", "OpenSSL_jll", "Zlib_jll"] +git-tree-sha1 = "57baa4b81a24c2910afbb6d853aa0685e4312bf7" +uuid = "9bd350c2-7e96-507f-8002-3f2e150b4e1b" +version = "10.3.1+0" + +[[deps.OpenSSL_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "458c3c95-2e84-50aa-8efc-19380b2a3a95" +version = "3.5.4+0" + +[[deps.PCRE2_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "efcefdf7-47ab-520b-bdef-62a2eaa19f15" +version = "10.44.0+1" + +[[deps.Parsers]] +deps = ["Dates", "PrecompileTools", "UUIDs"] +git-tree-sha1 = "5d5e0a78e971354b1c7bff0655d11fdc1b0e12c8" +uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" +version = "2.8.4" + +[[deps.Pkg]] +deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "Random", "SHA", "TOML", "Tar", "UUIDs", "p7zip_jll"] +uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" +version = "1.12.0" +weakdeps = ["REPL"] + + [deps.Pkg.extensions] + REPLExt = "REPL" + +[[deps.PrecompileTools]] +deps = ["Preferences"] +git-tree-sha1 = "07a921781cab75691315adc645096ed5e370cb77" +uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" +version = "1.3.3" + +[[deps.Preferences]] +deps = ["TOML"] +git-tree-sha1 = "8b770b60760d4451834fe79dd483e318eee709c4" +uuid = "21216c6a-2e73-6563-6e65-726566657250" +version = "1.5.2" + +[[deps.Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" +version = "1.11.0" + +[[deps.REPL]] +deps = ["InteractiveUtils", "JuliaSyntaxHighlighting", "Markdown", "Sockets", "StyledStrings", "Unicode"] +uuid = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" +version = "1.11.0" + +[[deps.Random]] +deps = ["SHA"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +version = "1.11.0" + +[[deps.RegistryInstances]] +deps = ["LazilyInitializedFields", "Pkg", "TOML", "Tar"] +git-tree-sha1 = "ffd19052caf598b8653b99404058fce14828be51" +uuid = "2792f1a3-b283-48e8-9a74-f99dce5104f3" +version = "0.1.0" + +[[deps.SHA]] +uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" +version = "0.7.0" + +[[deps.Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" +version = "1.11.0" + +[[deps.Sockets]] +uuid = "6462fe0b-24de-5631-8697-dd941f90decc" +version = "1.11.0" + +[[deps.StructUtils]] +deps = ["Dates", "UUIDs"] +git-tree-sha1 = "dd974aefe288ef2898733aecf40858dc86742d74" +uuid = "ec057cc2-7a8d-4b58-b3b3-92acb9f63b42" +version = "2.8.1" + + [deps.StructUtils.extensions] + StructUtilsMeasurementsExt = ["Measurements"] + StructUtilsStaticArraysCoreExt = ["StaticArraysCore"] + StructUtilsTablesExt = ["Tables"] + + [deps.StructUtils.weakdeps] + Measurements = "eff96d63-e80a-5855-80a2-b1b0885c5ab7" + StaticArraysCore = "1e83bf80-4336-4d27-bf5d-d5a4f845583c" + Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" + +[[deps.StyledStrings]] +uuid = "f489334b-da3d-4c2e-b8f0-e476e12c162b" +version = "1.11.0" + +[[deps.TOML]] +deps = ["Dates"] +uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" +version = "1.0.3" + +[[deps.Tar]] +deps = ["ArgTools", "SHA"] +uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" +version = "1.10.0" + +[[deps.Test]] +deps = ["InteractiveUtils", "Logging", "Random", "Serialization"] +uuid = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +version = "1.11.0" + +[[deps.TranscodingStreams]] +git-tree-sha1 = "0c45878dcfdcfa8480052b6ab162cdd138781742" +uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa" +version = "0.11.3" + +[[deps.UUIDs]] +deps = ["Random", "SHA"] +uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" +version = "1.11.0" + +[[deps.Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" +version = "1.11.0" + +[[deps.Zlib_jll]] +deps = ["Libdl"] +uuid = "83775a58-1f1d-513f-b197-d71354ab007a" +version = "1.3.1+2" + +[[deps.nghttp2_jll]] +deps = ["Artifacts", "Libdl"] +uuid = "8e850ede-7688-5339-a07c-302acd2aaf8d" +version = "1.64.0+1" + +[[deps.p7zip_jll]] +deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] +uuid = "3f19e933-33d8-53b3-aaab-bd5110c3b7a0" +version = "17.7.0+0" diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 0000000..1814eb3 --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,5 @@ +[deps] +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" + +[compat] +Documenter = "1" diff --git a/docs/cluster_models.md b/docs/cluster_models.md new file mode 100644 index 0000000..aa8133b --- /dev/null +++ b/docs/cluster_models.md @@ -0,0 +1,31 @@ +# Cluster Emission Models + +Sources: [`src/pam/2d/sources.jl`](../src/pam/2d/sources.jl), [`src/pam/3d/sources3d.jl`](../src/pam/3d/sources3d.jl) + +## BubbleCluster (2D and 3D) + +$$s(t) = A \;\cdot\; w_\text{Tukey}(t;\, T_\text{gate},\, r) \;\cdot\; \sum_{n \,\in\, \mathcal{H}} \alpha_n \cos\!\bigl(2\pi n f_0\, t + \phi_n\bigr), \qquad t \in [0,\, T_\text{gate}]$$ + +| Symbol | Field | 2D default | 3D default | +|---|---|---|---| +| $f_0$ | `fundamental` | 500 kHz | 500 kHz | +| $A$ | `amplitude` | 1.0 | 1.0 | +| $\mathcal{H}$ | `harmonics` | {2, 3} | {2, 3, 4} | +| $\alpha_n$ | `harmonic_amplitudes` | [1.0, 0.6] | [1.0, 0.6, 0.3] | +| $\phi_n$ | `harmonic_phases` | [0.0, 0.0] | [0.0, 0.0, 0.0] | +| $T_\text{gate}$ | `gate_duration` | 50 μs | 50 μs | +| $r$ | `taper_ratio` | 0.25 | 0.25 | + +## Term glossary + +| Symbol | Meaning | +|---|---| +| $t$ | Time, measured from source onset (after subtracting `delay`) | +| $f_0$ | Fundamental emission frequency. Harmonics are integer multiples $n f_0$ | +| $n$ | Harmonic order. Bubbles driven at $f_\text{drive}$ emit at $n f_\text{drive}$ | +| $A$ | Cluster pressure amplitude (linear scale) | +| $\alpha_n$ | Relative amplitude of harmonic $n$, normalised so $\alpha_2 = 1$ by convention | +| $\phi_n$ | Phase of harmonic $n$. Set by `phase_mode`: zero (coherent), geometric travel-time delay, or random | +| $T_\text{gate}$ | Total emission duration. Signal is zero outside $[0, T_\text{gate}]$ | +| $w_\text{Tukey}$ | Tukey (cosine-tapered rectangular) window. Taper fraction $r$ sets what fraction of the gate is the raised-cosine roll-on/off; $r=0$ is a rectangular gate, $r=1$ is a Hann window | + diff --git a/docs/generate_cli_reference.jl b/docs/generate_cli_reference.jl new file mode 100644 index 0000000..9068304 --- /dev/null +++ b/docs/generate_cli_reference.jl @@ -0,0 +1,70 @@ +module PAMCLIReference + +const DEFAULT_CT_PATH = normpath(joinpath(homedir(), "Desktop", "OBJ_0001")) +include(joinpath(dirname(@__DIR__), "src", "pam", "setup", "config.jl")) + +end + +function _md_cell(value) + text = string(value) + text = replace(text, "\n" => " ") + text = replace(text, "|" => "\\|") + return isempty(text) ? " " : text +end + +function _md_code(value) + text = string(value) + isempty(text) && return " " + return "`$(replace(text, "`" => "\\`"))`" +end + +function _choices_cell(option) + isempty(option.choices) && return " " + return join([_md_code(choice) for choice in option.choices], ", ") +end + +function _write_option_table(io, options) + println(io, "| Option | Default | Value | Applies to | Choices | Description |") + println(io, "|---|---:|---|---|---|---|") + for option in options + println(io, "| ", + _md_code("--" * option.name), " | ", + _md_code(option.default), " | ", + _md_cell(option.value), " | ", + _md_cell(option.applies_to), " | ", + _choices_cell(option), " | ", + _md_cell(option.description), " |") + end +end + +function generate_cli_reference(path=joinpath(@__DIR__, "src", "cli", "parameters.md")) + options = PAMCLIReference.pam_cli_options() + categories = unique(option.category for option in options) + mkpath(dirname(path)) + + open(path, "w") do io + println(io, "# PAM CLI Parameters") + println(io) + println(io, "This page is generated from the PAM CLI option metadata in `src/pam/setup/config.jl`.") + println(io, "Use options as `--name=value`; positional arguments are not supported.") + println(io) + println(io, "The listed defaults are the base defaults. `scripts/run_pam.jl` applies a few model-aware overrides after parsing:") + println(io) + println(io, "- `--dimension=3` defaults to `--source-model=point`, 3D coordinates, coarser `dy/dz`, and shorter `t-max-us` unless those options are provided.") + println(io, "- `--source-model=point` defaults to coherent phase, narrower receiver aperture, shorter duration, and `--recon-bandwidth-khz=0`.") + println(io, "- 3D `squiggle` and `network` runs default to windowed-friendly reconstruction settings such as `--recon-bandwidth-khz=40`, `--recon-window-us=40`, and `--recon-hop-us=20`.") + println(io) + println(io, "For practical guidance, start with [Running PAM](@ref) before tuning individual parameters.") + println(io) + + for category in categories + category_options = filter(option -> option.category == category, options) + println(io, "## ", category) + println(io) + _write_option_table(io, category_options) + println(io) + end + end + + return path +end diff --git a/docs/make.jl b/docs/make.jl new file mode 100644 index 0000000..0d0d5cc --- /dev/null +++ b/docs/make.jl @@ -0,0 +1,37 @@ +using Pkg + +Pkg.instantiate() + +using Documenter + +include("generate_cli_reference.jl") +generate_cli_reference() + +makedocs( + sitename="TranscranialFUS", + remotes=nothing, + format=Documenter.HTML( + prettyurls=get(ENV, "CI", "false") == "true", + edit_link=nothing, + repolink="", + ), + pages=[ + "Home" => "index.md", + "Getting Started" => "getting-started.md", + "CLI" => [ + "Overview" => "cli/overview.md", + "Running PAM" => "cli/run-pam.md", + "Running Focus" => "cli/run-focus.md", + "PAM Parameters" => "cli/parameters.md", + ], + "Workflow" => "workflow.md", + "Outputs" => "outputs.md", + "Validation" => "validation.md", + "Reference Notes" => [ + "PAM Algorithm" => "reference/pam-algorithm.md", + "Cluster Models" => "reference/cluster-models.md", + ], + "Troubleshooting" => "troubleshooting.md", + ], + checkdocs=:none, +) diff --git a/docs/pam_algorithm.md b/docs/pam_algorithm.md new file mode 100644 index 0000000..7abbff8 --- /dev/null +++ b/docs/pam_algorithm.md @@ -0,0 +1,141 @@ +# PAM Algorithm — High-Level Loop Structure + +This document describes the loop structure of the PAM (Photoacoustic Microscopy) +reconstruction algorithm, for both the CPU and GPU execution paths, and explains +how the two implementations correspond to each other. + +--- + +## What PAM does + +Starting from RF pressure data recorded at a receiver plane, PAM back-propagates +the acoustic field one axial row at a time and accumulates the intensity `|p|²` +at each row to form an image. The heterogeneous sound-speed field is handled by +the HASA correction; without it the method reduces to geometric ASA (a purely +linear phase shift). + +--- + +## CPU path — `_reconstruct_pam_cpu_3d` / `reconstruct_pam` (CPU branch) + +``` +for each frequency bin f # outer: spectral + build propagator, correction, eta operators + + current ← FFT(p₀) # initial condition from RF + + for each axial row (rr+1 → row_stop) # outer spatial: depth march + for each sub-step (1 → axial_substeps) # inner: stability substeps + + p_space ← IFFT(current) # back to spatial domain + ┌ HASA (corrected=true): + │ conv_term ← FFT(η[row] · p_space) + │ next ← current · prop + corr · conv_term + └ ASA (corrected=false): + next ← current · prop + + current ← next + + apply Tukey weighting once per row + + p_row ← IFFT(current) + intensity[row] += |p_row|² # accumulate + + (3D only) crop lateral window after row loop +``` + +**Loop nesting (2D):** `freq → row → substep` +**Loop nesting (3D):** `freq → row → substep → (iy, iz) for crop` + +Key points: +- Everything is serial; FFTW plans are measured once per call and reused. +- `eta = 1 − (c₀/c)²` — the speed-contrast field driving the HASA term. +- The Tukey weighting is applied once *per row* (not per substep) to suppress + long-range numerical growth without coupling damping to substep count. +- Evanescent modes (imaginary `k_axial`) are zeroed before accumulation. + +--- + +## GPU path — `_reconstruct_pam_cuda` + +The GPU version restructures the same computation to maximise memory throughput +by **batching all frequency bins and all time-windows together** into a single +2-D array swept through the axial march. + +``` +build p0_d (padded_ny, nfreq×W) # pack ALL freq×window ICs at once + +current_d ← FFT(p0_d, lateral_dim) # batched lateral FFT + +for each axial row (rr+1 → row_stop) # same depth march as CPU + ┌ HASA (corrected=true): + │ for each sub-step (1 → axial_substeps-1) + │ p_space_d ← IFFT(current_d) # HASA + │ tmp_d ← k0²_d · η[row] · p_space_d # HASA + │ FFT(tmp_d) # HASA + │ next_d ← current_d · prop_d + corr_d · tmp_d # HASA + │ swap current_d ↔ next_d + │ final sub-step (uses prop_weight_d, corr_weight_d) + └ ASA (corrected=false): + current_d .*= prop_n_weight_d # single element-wise multiply + + p_row_d ← IFFT(current_d) + _accum_abs2_sum_batched! kernel: # custom CUDA kernel + for each lateral index i (one GPU thread): + for each window w: + for each freq f within window: + intensity[i, w, row] += |p_row_d[i, f]|² +``` + +**Loop nesting:** `row → substep` (on CPU dispatch) → GPU threads handle all +lateral positions `i` in parallel; frequency and window loops inside the kernel. + +Key points: +- `(padded_ny, nfreq×W)` layout means every element-wise op hits the full + batch with a single cuBLAS/broadcast call — no per-frequency dispatch overhead. +- `eta_yx_d` (2D) / `eta_yznx_d` (3D) is sliced per row as `[:, row:row]`; + broadcasting makes this zero-copy. +- The final substep uses pre-fused `prop_weight_d = prop_d .* weight_d` to save + one kernel launch. +- The `_accum_abs2_sum_batched!` kernel accumulates across the frequency + dimension *on the GPU*, so only the finished `intensity_yWx_d` is downloaded. + +--- + +## CPU → GPU correspondence + +| CPU concept | GPU equivalent | Notes | +|---|---|---| +| `for (freq, bin) in ...` outer loop | Packed into dim-2 of `current_d` | All freqs live in one array; no dispatch loop | +| `for w in windows` (windowed API) | Packed into same dim-2 as `W` blocks | `nfreq_W = nfreq × W` columns | +| `ifft(current)` | `plan_bwd * p_space_d` | In-place cuFFT on the whole batch | +| `fft(η[row] .* p_space)` | `tmp_d .= k0²_d .* η[row:row] .* p_space_d; plan_fwd * tmp_d` | Row slice broadcasts over all freq×window columns | +| `current .* prop .+ corr .* conv_term` | `next_d .= current_d .* prop_d .+ corr_d .* tmp_d` | Element-wise; `prop_d` / `corr_d` are tiled `W` times at setup | +| `current .*= weighting` once per row | Fused into `prop_weight_d`, `corr_weight_d`, `prop_n_weight_d` | Saves a separate broadcast pass | +| `out[row, :] .+= abs2.(p_row)` | `_accum_abs2_sum_batched!` CUDA kernel | Kernel reduces over freq inside GPU; result goes to `intensity_yWx_d[:, :, row]` | +| Inner `for _ in 1:axial_substeps` | Same loop on CPU dispatch thread | Substep loop stays on host; each iteration launches batch GPU ops | +| CPU downloads result each row | Single download after all rows | `Array(intensity_yWx_d)` + permutedims at the end | + +--- + +## Windowed reconstruction + +`reconstruct_pam_windowed` wraps both paths: + +1. Partition the full RF time axis into overlapping windows. +2. Skip windows below an energy threshold. +3. For GPU + `window_batch > 1`: build the CUDA setup once, then submit batches + of `window_batch` windows at a time through `_reconstruct_pam_cuda`. +4. Accumulate each batch's intensity into the running `intensity` sum. + +The inner reconstruction is identical to the single-window case — the windowing +layer only controls *which* slices of RF are fed in and how results are summed. + +--- + +## Wave propagation + +Each call to `simulate_point_sources` / `simulate_point_sources_3d` runs a +k-Wave acoustic simulation (**HASA**) to produce the synthetic RF data that the +reconstruction consumes. The PAM loop structure above is the *reconstruction* +side; it never calls k-Wave directly. diff --git a/docs/src/cli/overview.md b/docs/src/cli/overview.md new file mode 100644 index 0000000..4976436 --- /dev/null +++ b/docs/src/cli/overview.md @@ -0,0 +1,56 @@ +# CLI Overview + +The maintained entry point is: + +```bash +julia --project=. scripts/run_pam.jl --option=value +``` + +The focusing scripts are also available: + +```bash +julia --project=. scripts/run_focus.jl --option=value +julia --project=. scripts/compare_focus_estimators.jl --option=value +``` + +All scripts use `--name=value` arguments. Positional arguments and space-separated forms such as `--name value` are not supported. + +## Output Directories + +Runs write to an automatically named directory under `outputs/`. The name includes the date, main run type, medium, source model, grid size, and other identifying parameters. Use `--out-dir=/path/to/output` when a fixed location is needed. + +Typical outputs include: + +- `summary.json`: run settings, source metadata, metrics, and output paths. +- `result.jld2`: numerical arrays and structured data for later analysis. +- `overview.png`: PAM source/reconstruction overview where applicable. +- `activity_boundaries.png`: detection-threshold visualization for activity sources. +- `pressure.png` or `comparison.png`: focusing figures. + +## GPU Flags + +PAM separates the forward simulator and reconstruction GPU switches: + +- `--kwave-use-gpu=true` controls k-Wave forward simulation. +- `--recon-use-gpu=true` controls CUDA.jl ASA/HASA reconstruction. + +Older notes may mention `--use-gpu`; the current PAM runner uses the two explicit flags above. + +## Coordinate Conventions + +PAM coordinates are specified in millimeters relative to the receiver/transducer plane: + +- 2D point or anchor coordinates use `depth:lateral`. +- 3D point or anchor coordinates use `depth:y:z`. +- Comma-separated values specify multiple coordinates. + +Examples: + +```bash +--sources-mm=30:0 +--sources-mm=25:-6,32:0,40:8 +--sources-mm=30:2:-1 +--anchors-mm=45:0:0 +``` + +Depth is positive into the medium. Lateral coordinates are centered around the transducer/receiver axis. diff --git a/docs/src/cli/parameters.md b/docs/src/cli/parameters.md new file mode 100644 index 0000000..41f93c8 --- /dev/null +++ b/docs/src/cli/parameters.md @@ -0,0 +1,159 @@ +# PAM CLI Parameters + +This page is generated from the PAM CLI option metadata in `src/pam/setup/config.jl`. +Use options as `--name=value`; positional arguments are not supported. + +The listed defaults are the base defaults. `scripts/run_pam.jl` applies a few model-aware overrides after parsing: + +- `--dimension=3` defaults to `--source-model=point`, 3D coordinates, coarser `dy/dz`, and shorter `t-max-us` unless those options are provided. +- `--source-model=point` defaults to coherent phase, narrower receiver aperture, shorter duration, and `--recon-bandwidth-khz=0`. +- 3D `squiggle` and `network` runs default to windowed-friendly reconstruction settings such as `--recon-bandwidth-khz=40`, `--recon-window-us=40`, and `--recon-hop-us=20`. + +For practical guidance, start with [Running PAM](@ref) before tuning individual parameters. + +## General + +| Option | Default | Value | Applies to | Choices | Description | +|---|---:|---|---|---|---| +| `--dimension` | `2` | 2\|3 | PAM | `2`, `3` | Selects the 2D or 3D PAM workflow. | +| `--source-model` | `squiggle` | point\|squiggle\|network | PAM | `point`, `squiggle`, `network` | Selects explicit point sources, a squiggly vascular source, or a synthetic 3D network. | +| `--from-run-dir` | | path | 2D reconstruction only | | Loads RF data, medium, grid, and sources from a previous output directory and reruns reconstruction/analysis only. | +| `--random-seed` | `42` | integer | PAM | | Seed used for stochastic phases, source placement jitter, and generated vascular/network geometry. | +| `--benchmark` | `false` | bool | PAM | | Prints additional timing information from simulation and reconstruction. | + +## Source geometry + +| Option | Default | Value | Applies to | Choices | Description | +|---|---:|---|---|---|---| +| `--sources-mm` | `30:0` | depth:lateral[,depth:lateral] or depth:y:z | point | | Point source coordinates in millimeters. 2D uses depth:lateral; 3D uses depth:y:z. | +| `--anchors-mm` | `45:0` | depth:lateral[,depth:lateral] or depth:y:z | squiggle, network | | Anchor coordinates for generated vascular or network activity. 2D uses depth:lateral; 3D uses depth:y:z. | +| `--transducer-mm` | `-30:0` | depth:lateral | 2D squiggle | | Reference transducer position used when computing geometric source phases in 2D. | + +## Source signal + +| Option | Default | Value | Applies to | Choices | Description | +|---|---:|---|---|---|---| +| `--frequency-mhz` | `0.4` | MHz | point | | Tone-burst frequency for point sources unless per-source frequencies are supplied. | +| `--fundamental-mhz` | `0.5` | MHz | squiggle, network | | Fundamental activity frequency. Harmonic frequencies are integer multiples of this value. | +| `--amplitude-pa` | `1.0` | pressure | PAM | | Default pressure amplitude for generated sources. | +| `--source-amplitudes-pa` | | comma list | point | | Optional per-point-source amplitudes. Use one value for all sources or one value per source. | +| `--source-frequencies-mhz` | | comma list | point | | Optional per-point-source frequencies in MHz. Use one value for all sources or one value per source. | +| `--phases-deg` | | comma list | point | | Optional per-point-source phases in degrees before phase-mode randomization. | +| `--delays-us` | `0` | comma list | PAM | | Emission delays in microseconds. Use one value for all sources or one value per coordinate/anchor. | +| `--num-cycles` | `4` | integer | point | | Number of cycles in each point-source tone burst. | +| `--harmonics` | `2,3,4` | comma list | squiggle, network | | Harmonic orders emitted by generated bubble activity. | +| `--harmonic-amplitudes` | `1.0,0.6,0.3` | comma list | squiggle, network | | Relative amplitude for each harmonic listed in --harmonics. | +| `--gate-us` | `50` | microseconds | squiggle, network | | Duration of each activity emission gate. | +| `--taper-ratio` | `0.25` | fraction | squiggle, network | | Tukey taper fraction applied to generated activity gates. | +| `--phase-mode` | `geometric` | coherent\|random\|jittered\|geometric | PAM | `coherent`, `random`, `jittered`, `geometric` | Controls initial source phases. Point sources accept coherent, random, and jittered; generated activity also uses geometric travel-time phases. | +| `--phase-jitter-rad` | `0.2` | radians | PAM | | Standard deviation for jittered source phases. | +| `--source-phase-mode` | `random_phase_per_window` | coherent\|random_static_phase\|random_phase_per_window | PAM | `coherent`, `random_static_phase`, `random_phase_per_window` | Controls whether source phases are fixed or redrawn across reconstruction windows. | +| `--frequency-jitter-percent` | `1` | percent | squiggle, network | | Multiplicative jitter applied to generated source fundamentals before harmonics are formed. | + +## Vascular source + +| Option | Default | Value | Applies to | Choices | Description | +|---|---:|---|---|---|---| +| `--vascular-length-mm` | `12` | mm | squiggle | | Length of the generated squiggle centerline for each anchor. | +| `--vascular-squiggle-amplitude-mm` | `1.5` | mm | squiggle | | Lateral squiggle amplitude in 2D, or y-amplitude in 3D. | +| `--vascular-squiggle-amplitude-x-mm` | `1.0` | mm | 3D squiggle | | Depth-direction squiggle amplitude for 3D vascular sources. | +| `--vascular-squiggle-wavelength-mm` | `8` | mm | squiggle | | Spatial wavelength of the generated squiggle path. | +| `--vascular-squiggle-slope` | `0.0` | slope | squiggle | | Linear slope added to the generated squiggle path. | +| `--squiggle-phase-x-deg` | `90` | degrees | 3D squiggle | | Phase offset for the 3D depth-direction squiggle component. | +| `--vascular-source-spacing-mm` | `0.5` | mm | squiggle, network | | Approximate spacing between sampled bubble emitters along generated centerlines. | +| `--vascular-position-jitter-mm` | `0.05` | mm | squiggle | | Random position jitter applied when sampling vascular sources. | +| `--vascular-min-separation-mm` | `0.25` | mm | squiggle, network | | Minimum allowed distance between generated bubble emitters. | +| `--vascular-max-sources-per-anchor` | `0` | integer | squiggle | | Caps generated sources per anchor. A value of 0 disables the cap. | + +## Analysis + +| Option | Default | Value | Applies to | Choices | Description | +|---|---:|---|---|---|---| +| `--vascular-radius-mm` | `1.0` | mm | squiggle, network | | Truth radius used when scoring activity detection around generated sources. | +| `--peak-suppression-radius-mm` | `8.0` | mm | PAM | | Radius used to suppress neighboring peaks during localization analysis. | +| `--success-tolerance-mm` | `1.5` | mm | PAM | | Localization error threshold used when reporting success. | +| `--axial-gain-power` | `1.5` | power | 3D | | Depth-gain exponent applied in 3D analysis/visualization. | +| `--analysis-mode` | `auto` | auto\|localization\|detection | PAM | `auto`, `localization`, `detection` | Selects localization or activity-detection metrics. Auto uses detection for squiggle/network sources. | +| `--detection-threshold-ratio` | `0.2` | ratio | detection | | Single threshold ratio used by basic detection analysis. | +| `--boundary-threshold-ratios` | `0.5,0.55,0.6,0.65,0.7,0.75` | comma list | detection | | Threshold ratios used for boundary overlays and threshold sweeps. | +| `--auto-threshold-search` | `true` | bool | detection | | Searches a dense threshold range and selects representative detection thresholds. | +| `--auto-threshold-min` | `0.10` | ratio | detection | | Minimum threshold ratio for automatic threshold search. | +| `--auto-threshold-max` | `0.95` | ratio | detection | | Maximum threshold ratio for automatic threshold search. | +| `--auto-threshold-step` | `0.01` | ratio | detection | | Threshold ratio spacing for automatic threshold search. | + +## Network source + +| Option | Default | Value | Applies to | Choices | Description | +|---|---:|---|---|---|---| +| `--network-axial-radius-mm` | `10.0` | mm | 3D network | | Axial radius of the ellipsoid used to clip generated network activity. | +| `--network-lateral-y-radius-mm` | `1.5` | mm | 3D network | | Y radius of the generated network ellipsoid. | +| `--network-lateral-z-radius-mm` | `1.5` | mm | 3D network | | Z radius of the generated network ellipsoid. | +| `--network-root-count` | `12` | integer | 3D network | | Number of root branches grown for each network center. | +| `--network-generations` | `3` | integer | 3D network | | Number of branching generations in the synthetic network. | +| `--network-branch-length-mm` | `5.0` | mm | 3D network | | Nominal length of each generated branch segment. | +| `--network-branch-step-mm` | `0.4` | mm | 3D network | | Sampling step along generated network branches. | +| `--network-branch-angle-deg` | `36` | degrees | 3D network | | Nominal branching angle for synthetic network growth. | +| `--network-tortuosity` | `0.18` | fraction | 3D network | | Strength of random branch curvature in the synthetic network. | +| `--network-orientation` | `isotropic` | isotropic\|horizontal\|axial | 3D network | `isotropic`, `horizontal`, `axial` | Orientation prior for generated network branches. | +| `--network-density-sigma-mm` | `0` | mm | 3D network | | Optional isotropic Gaussian density sigma. A value of 0 uses the anisotropic sigma options. | +| `--network-density-axial-sigma-mm` | `10.0` | mm | 3D network | | Axial Gaussian density sigma for network source sampling. | +| `--network-density-lateral-y-sigma-mm` | `1.5` | mm | 3D network | | Y Gaussian density sigma for network source sampling. | +| `--network-density-lateral-z-sigma-mm` | `1.5` | mm | 3D network | | Z Gaussian density sigma for network source sampling. | +| `--network-max-sources-per-center` | `80` | integer | 3D network | | Caps generated sources per network center. Values <= 0 disable the cap. | + +## Grid + +| Option | Default | Value | Applies to | Choices | Description | +|---|---:|---|---|---|---| +| `--axial-mm` | `80` | mm | PAM | | Requested axial domain depth. The runner may extend this to fit sources and time of flight. | +| `--transverse-mm` | `102.4` | mm | PAM | | Default lateral domain width. In 3D this seeds y and z widths unless overridden. | +| `--transverse-y-mm` | | mm | 3D | | Overrides the 3D y-width when set. | +| `--transverse-z-mm` | | mm | 3D | | Overrides the 3D z-width when set. | +| `--dx-mm` | `0.2` | mm | PAM | | Axial grid spacing. | +| `--dy-mm` | | mm | 3D | | 3D y grid spacing. Defaults to --dz-mm when omitted. | +| `--dz-mm` | `0.2` | mm | PAM | | 2D lateral spacing or 3D z spacing. | +| `--t-max-us` | `500` | microseconds | PAM | | Requested simulation duration. The runner may extend this when needed to capture source arrivals. | +| `--dt-ns` | `20` | nanoseconds | PAM | | Simulation time step. | +| `--zero-pad-factor` | `4` | integer | PAM | | Lateral FFT zero-padding factor used by ASA/HASA reconstruction. | +| `--bottom-margin-mm` | `10` | mm | PAM | | Minimum margin below the deepest source when auto-fitting the PAM domain. | + +## Receiver + +| Option | Default | Value | Applies to | Choices | Description | +|---|---:|---|---|---|---| +| `--receiver-aperture-mm` | `full` | mm\|full | PAM | | Receiver aperture width. Use full, all, or none to use the whole receiver plane. | +| `--receiver-aperture-y-mm` | | mm\|full | 3D | | Overrides the 3D receiver aperture in y. | +| `--receiver-aperture-z-mm` | | mm\|full | 3D | | Overrides the 3D receiver aperture in z. | + +## Medium + +| Option | Default | Value | Applies to | Choices | Description | +|---|---:|---|---|---|---| +| `--aberrator` | `none` | none\|water\|skull | PAM | `none`, `water`, `skull` | Selects homogeneous water/no aberrator or a CT-derived skull medium. | +| `--ct-path` | `/Users/vm/Desktop/OBJ_0001` | path | skull | | Path to the private DICOM folder used for CT-backed skull media. | +| `--slice-index` | `250` | integer | skull | | CT slice index used when building the skull medium. | +| `--skull-transducer-distance-mm` | `30` | mm | skull | | Distance from the receiver/transducer plane to the outer skull surface. | +| `--hu-bone-thr` | `200` | HU | skull | | Hounsfield-unit threshold used to identify bone in CT data. | + +## Simulation + +| Option | Default | Value | Applies to | Choices | Description | +|---|---:|---|---|---|---| +| `--simulation-backend` | `kwave` | kwave\|analytic | PAM | `kwave`, `analytic` | Forward model backend. CT skull runs require k-Wave. | +| `--kwave-use-gpu` | `true` | bool | k-Wave | | Passes GPU execution to k-Wave where supported. | + +## Reconstruction + +| Option | Default | Value | Applies to | Choices | Description | +|---|---:|---|---|---|---| +| `--recon-use-gpu` | `true` | bool | PAM | | Uses the CUDA.jl reconstruction backend. 3D reconstruction currently requires this to be true. | +| `--recon-bandwidth-khz` | `500` | kHz | PAM | | Half-width bandwidth used to select frequency bins around reconstruction frequencies. Use 0 to keep only the target bins. | +| `--recon-step-um` | `50` | micrometers | PAM | | Axial integration step used by ASA/HASA reconstruction. | +| `--recon-mode` | `auto` | auto\|full\|windowed | PAM | `auto`, `full`, `windowed` | Reconstruction mode. Auto uses full for point sources and windowed for squiggle/network activity. | +| `--recon-window-us` | `20` | microseconds | windowed | | Window duration for windowed incoherent reconstruction. | +| `--recon-hop-us` | `10` | microseconds | windowed | | Hop between consecutive reconstruction windows. | +| `--recon-window-taper` | `hann` | hann\|none\|rectangular\|tukey | windowed | `hann`, `none`, `rectangular`, `tukey` | Taper applied to each reconstruction window. | +| `--recon-min-window-energy-ratio` | `0.001` | ratio | windowed | | Skips windows whose energy is below this fraction of the maximum window energy. | +| `--recon-progress` | `false` | bool | PAM | | Prints reconstruction progress updates. | +| `--window-batch` | `1` | integer | windowed GPU | | Number of reconstruction windows batched together on the GPU. | + diff --git a/docs/src/cli/run-focus.md b/docs/src/cli/run-focus.md new file mode 100644 index 0000000..c7c0832 --- /dev/null +++ b/docs/src/cli/run-focus.md @@ -0,0 +1,62 @@ +# Running Focus + +The focusing scripts are retained from the earlier transcranial focusing workflow. They are useful for comparing geometric and HASA focusing corrections through water or a CT-backed skull medium. + +## Single Focusing Case + +Default skull-backed HASA case: + +```bash +julia --project=. scripts/run_focus.jl \ + --estimator=hasa \ + --medium=skull_in_water \ + --slice-index=250 +``` + +Centered target at 60 mm below the transducer: + +```bash +julia --project=. scripts/run_focus.jl \ + --estimator=hasa \ + --medium=skull_in_water \ + --placement=fixed_transducer \ + --slice-index=250 \ + --lateral-cm=0.0 \ + --focal-cm=6.0 +``` + +Target 30 mm below the inner skull: + +```bash +julia --project=. scripts/run_focus.jl \ + --estimator=hasa \ + --medium=skull_in_water \ + --placement=fixed_focus_depth \ + --slice-index=250 \ + --focal-cm=6.0 \ + --focus-depth-from-inner-skull-mm=30 +``` + +## Compare Estimators + +```bash +julia --project=. scripts/compare_focus_estimators.jl \ + --medium=skull_in_water \ + --slice-index=250 +``` + +## Main Options + +- `--ct-path`: DICOM folder for CT-backed skull runs. +- `--slice-index`: CT slice used for the 2D focusing medium. +- `--frequency-mhz`: transmit frequency. +- `--focal-cm`: transducer-to-focus distance for fixed-transducer placement, or transducer distance to the resolved target for fixed-depth placement. +- `--lateral-cm`: lateral target offset. +- `--aperture-cm`: transducer aperture width. +- `--estimator`: `geometric` or `hasa`. +- `--medium`: `water` or `skull_in_water`. +- `--placement`: `auto`, `fixed_transducer`, or `fixed_focus_depth`. +- `--focus-depth-from-inner-skull-mm`: target depth below the inner skull for fixed-depth placement. +- `--out-dir`: override the automatically generated output directory. + +`scripts/run_focus.jl` writes `summary.json`, `result.jld2`, and `pressure.png`. `scripts/compare_focus_estimators.jl` writes `summary.json` and `comparison.png`. diff --git a/docs/src/cli/run-pam.md b/docs/src/cli/run-pam.md new file mode 100644 index 0000000..37e5f75 --- /dev/null +++ b/docs/src/cli/run-pam.md @@ -0,0 +1,111 @@ +# Running PAM + +`scripts/run_pam.jl` simulates passive acoustic emissions and reconstructs source activity from receiver RF data. It supports 2D and 3D domains, homogeneous water controls, CT-backed skull media, sparse point sources, and generated vascular activity. + +## Homogeneous Point Source + +Use this when checking the installation or testing reconstruction behavior without CT data: + +```bash +julia --project=. scripts/run_pam.jl \ + --source-model=point \ + --sources-mm=30:0 \ + --aberrator=none \ + --recon-use-gpu=false +``` + +For multiple 2D point emitters: + +```bash +julia --project=. scripts/run_pam.jl \ + --source-model=point \ + --sources-mm=25:-6,32:0,40:8 \ + --phases-deg=0,90,180 \ + --delays-us=0,2,4 \ + --aberrator=none +``` + +## 3D Point Source + +3D reconstruction currently uses the CUDA backend: + +```bash +julia --project=. scripts/run_pam.jl \ + --dimension=3 \ + --source-model=point \ + --sources-mm=30:2:-1 \ + --aberrator=none \ + --recon-use-gpu=true +``` + +## Squiggle Activity + +`--source-model=squiggle` expands each anchor into a generated vascular centerline and samples bubble emitters along it. In `--recon-mode=auto`, squiggle runs use windowed incoherent reconstruction. + +```bash +julia --project=. scripts/run_pam.jl \ + --source-model=squiggle \ + --anchors-mm=45:0 \ + --aberrator=skull \ + --skull-transducer-distance-mm=30 \ + --slice-index=250 \ + --source-phase-mode=random_phase_per_window \ + --frequency-jitter-percent=1 \ + --recon-bandwidth-khz=500 \ + --recon-window-us=20 \ + --recon-hop-us=10 \ + --recon-progress=true +``` + +## 3D Network Activity + +`--source-model=network` creates a random branching 3D centerline structure around each anchor and samples emitters inside an ellipsoid. + +```bash +julia --project=. scripts/run_pam.jl \ + --dimension=3 \ + --source-model=network \ + --anchors-mm=45:0:0 \ + --network-root-count=12 \ + --network-generations=3 \ + --aberrator=skull \ + --skull-transducer-distance-mm=20 \ + --slice-index=250 \ + --recon-bandwidth-khz=40 \ + --recon-window-us=40 \ + --recon-hop-us=20 \ + --recon-use-gpu=true \ + --recon-progress=true +``` + +## Reconstruction-Only Mode + +For 2D runs, `--from-run-dir` reuses a previous `result.jld2` and reruns reconstruction and analysis without rerunning k-Wave: + +```bash +julia --project=. scripts/run_pam.jl \ + --from-run-dir=outputs/previous_pam_run \ + --recon-bandwidth-khz=20 \ + --recon-use-gpu=true +``` + +In this mode, simulation-specific options such as source locations, medium/skull settings, grid size, and time step are rejected. Reconstruction and analysis options remain adjustable. + +## Choosing Important Parameters + +Start with the source model and medium: + +- Use `--source-model=point` for localization tests. +- Use `--source-model=squiggle` for generated vascular activity. +- Use `--source-model=network` for 3D branching activity. +- Use `--aberrator=none` for homogeneous controls. +- Use `--aberrator=skull` with `--ct-path`, `--slice-index`, and `--skull-transducer-distance-mm` for CT-backed transcranial cases. + +Then tune reconstruction: + +- `--recon-mode=auto` is usually the right default. +- `--recon-bandwidth-khz` is a major runtime/accuracy knob. +- `--recon-window-us` and `--recon-hop-us` control windowed incoherent reconstruction. +- `--window-batch` can improve GPU throughput for windowed reconstruction. + +See [PAM CLI Parameters](@ref) for the generated option reference. diff --git a/docs/src/getting-started.md b/docs/src/getting-started.md new file mode 100644 index 0000000..c0cb960 --- /dev/null +++ b/docs/src/getting-started.md @@ -0,0 +1,56 @@ +# Getting Started + +## Requirements + +Use Julia 1.12 with the project environment at the repository root. + +```bash +julia --project=. -e 'using Pkg; Pkg.instantiate()' +``` + +The forward simulation path uses Python packages managed by `CondaPkg.toml`, including `k-wave-python`, `numpy`, and `scipy`. The first call into the k-Wave wrapper may resolve Python packages and k-Wave resources. + +CUDA-backed reconstruction requires an NVIDIA GPU visible to CUDA.jl. 3D PAM reconstruction currently requires `--recon-use-gpu=true`. + +## CT Data + +The private CT data used during development is not distributed with this repository. By default, scripts look for a DICOM folder under: + +```text +~/Desktop/OBJ_0001 +``` + +Override this with `--ct-path=/path/to/dicom-folder` for skull-backed runs. Homogeneous runs with `--aberrator=none` do not need CT data. + +## First PAM Run + +A small homogeneous point-source run is the fastest way to confirm the CLI works: + +```bash +julia --project=. scripts/run_pam.jl \ + --source-model=point \ + --sources-mm=30:0 \ + --aberrator=none \ + --recon-use-gpu=false +``` + +For 3D, use a CUDA-capable machine: + +```bash +julia --project=. scripts/run_pam.jl \ + --dimension=3 \ + --source-model=point \ + --sources-mm=30:2:-1 \ + --aberrator=none \ + --recon-use-gpu=true +``` + +## Tests + +Run the standard test suite from the repository root: + +```bash +julia --project=. -e 'using Pkg; Pkg.test()' +``` + +Optional k-Wave and CT-backed tests are controlled by environment variables documented in the repository README. diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 0000000..b202db7 --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,14 @@ +# TranscranialFUS Documentation + +`TranscranialFUS` is a Julia project for transcranial ultrasound focusing and passive acoustic mapping (PAM). The repository is intended to be used primarily through the scripts in `scripts/`, especially `scripts/run_pam.jl`. + +The documentation is organized around the command-line workflow: + +- [Getting Started](@ref) covers setup, data assumptions, and first smoke runs. +- [CLI Overview](@ref) explains common command syntax and runtime behavior. +- [Running PAM](@ref) is the main user guide for `scripts/run_pam.jl`. +- [Running Focus](@ref) documents the focusing scripts kept from the earlier workflow. +- [PAM CLI Parameters](@ref) is generated from CLI option metadata in the package. +- [High-Level Workflow](@ref) explains what happens under the hood without going into every internal function. + +The code is research-oriented and assumes access to local CT data for skull-backed examples. Homogeneous water examples do not require CT data. diff --git a/docs/src/outputs.md b/docs/src/outputs.md new file mode 100644 index 0000000..75121d3 --- /dev/null +++ b/docs/src/outputs.md @@ -0,0 +1,35 @@ +# Outputs + +Scripts write run artifacts to `outputs/` unless `--out-dir` is provided. + +## PAM Outputs + +`scripts/run_pam.jl` typically writes: + +- `summary.json`: human-readable configuration, source metadata, medium metadata, reconstruction settings, timing information, and analysis metrics. +- `result.jld2`: numerical arrays and structured data needed for later reconstruction or analysis. +- `overview.png`: source, RF, and reconstruction overview where available. +- `activity_boundaries.png`: threshold-dependent active-region boundaries and detection curves for squiggle/network runs. +- `best_threshold_3d.png`: selected 3D threshold visualization where applicable. +- `napari_volume.npz`: optional 3D visualization volume when generated by the run path. + +`--from-run-dir` creates a new reconstruction output folder and reuses the previous `result.jld2` RF data, medium, grid, and source metadata. + +## Focusing Outputs + +`scripts/run_focus.jl` writes: + +- `summary.json` +- `result.jld2` +- `pressure.png` + +`scripts/compare_focus_estimators.jl` writes: + +- `summary.json` +- `comparison.png` + +## Reading `summary.json` + +The summary file is the best first artifact to inspect. It records the resolved configuration, not just the typed CLI arguments. For PAM, this is important because `fit_pam_config` may extend the axial domain and simulation duration to fit the selected sources. + +For generated activity sources, `summary.json` also records source counts, centerline metadata, phase/frequency variability, window configuration, and threshold-search results. diff --git a/docs/src/reference/cluster-models.md b/docs/src/reference/cluster-models.md new file mode 100644 index 0000000..00a77c4 --- /dev/null +++ b/docs/src/reference/cluster-models.md @@ -0,0 +1,38 @@ +# Cluster Emission Models + +Source implementations are in `src/pam/2d/sources.jl` and `src/pam/3d/sources3d.jl`. + +## Bubble Cluster + +For squiggle and network activity, generated bubble clusters emit a tapered harmonic signal: + +```math +s(t) = A \cdot w_\mathrm{Tukey}(t; T_\mathrm{gate}, r) \cdot \sum_{n \in H} \alpha_n \cos(2\pi n f_0 t + \phi_n) +``` + +The signal is active during the configured gate and zero outside it. + +## Parameters + +| Symbol | Field | 2D default | 3D default | +|---|---|---:|---:| +| `f0` | `fundamental` | 500 kHz | 500 kHz | +| `A` | `amplitude` | 1.0 | 1.0 | +| `H` | `harmonics` | `{2, 3}` | `{2, 3, 4}` | +| `alpha_n` | `harmonic_amplitudes` | `[1.0, 0.6]` | `[1.0, 0.6, 0.3]` | +| `phi_n` | `harmonic_phases` | `[0.0, 0.0]` | `[0.0, 0.0, 0.0]` | +| `T_gate` | `gate_duration` | 50 us | 50 us | +| `r` | `taper_ratio` | 0.25 | 0.25 | + +## Term Glossary + +| Term | Meaning | +|---|---| +| `t` | Time measured from source onset after subtracting source delay. | +| `f0` | Fundamental emission frequency; harmonics are integer multiples of it. | +| `n` | Harmonic order. | +| `A` | Cluster pressure amplitude on a linear scale. | +| `alpha_n` | Relative amplitude of harmonic `n`. | +| `phi_n` | Phase of harmonic `n`, set by the selected phase mode. | +| `T_gate` | Total emission duration. | +| `w_Tukey` | Tapered rectangular window. `r=0` is rectangular; `r=1` is Hann-like. | diff --git a/docs/src/reference/pam-algorithm.md b/docs/src/reference/pam-algorithm.md new file mode 100644 index 0000000..4b634ea --- /dev/null +++ b/docs/src/reference/pam-algorithm.md @@ -0,0 +1,90 @@ +# PAM Algorithm + +This note describes the high-level loop structure of the PAM reconstruction algorithm, for both CPU and GPU execution paths, and explains how the two implementations correspond to each other. + +## What PAM Does + +Starting from RF pressure data recorded at a receiver plane, PAM back-propagates the acoustic field one axial row at a time and accumulates the intensity `|p|^2` at each row to form an image. The heterogeneous sound-speed field is handled by the HASA correction. Without it, the method reduces to geometric ASA. + +## CPU Path + +The CPU reconstruction follows a direct frequency-by-frequency march: + +```text +for each frequency bin f + build propagator, correction, eta operators + current <- FFT(p0) + + for each axial row + for each axial sub-step + p_space <- IFFT(current) + if corrected + conv_term <- FFT(eta[row] * p_space) + next <- current * prop + corr * conv_term + else + next <- current * prop + end + current <- next + end + + apply row weighting + p_row <- IFFT(current) + intensity[row] += |p_row|^2 + end +end +``` + +Key points: + +- Loop nesting is `frequency -> row -> substep`. +- `eta = 1 - (c0 / c)^2` is the speed-contrast field driving the HASA term. +- Tukey weighting is applied once per row to suppress long-range numerical growth. +- Evanescent modes are zeroed before accumulation. + +## GPU Path + +The GPU version restructures the same computation by batching all selected frequency bins and time windows together into one array swept through the axial march. + +```text +build p0_d with all frequency/window initial conditions +current_d <- FFT(p0_d) + +for each axial row + if corrected + for each axial sub-step + p_space_d <- IFFT(current_d) + tmp_d <- k0^2 * eta[row] * p_space_d + FFT(tmp_d) + next_d <- current_d * prop_d + corr_d * tmp_d + swap current_d and next_d + end + else + current_d .*= prop_weight_d + end + + p_row_d <- IFFT(current_d) + accumulate |p_row_d|^2 across frequency/window batches +end +``` + +Key points: + +- Frequency and window dimensions are packed into the second dimension of the GPU work array. +- Element-wise operations hit the full batch with one broadcast or kernel call. +- The final row weighting is fused into precomputed propagator/correction weights. +- The accumulated intensity is downloaded after the march instead of row by row. + +## Windowed Reconstruction + +`reconstruct_pam_windowed` wraps the single-window reconstruction: + +1. Partition RF data into overlapping windows. +2. Skip windows below the configured energy threshold. +3. Reconstruct each active window. +4. Accumulate window intensity into the output image. + +The inner ASA/HASA march is the same as a full reconstruction; windowing only controls which RF slices are fed into it and how their intensities are accumulated. + +## Forward Simulation + +`simulate_point_sources` and `simulate_point_sources_3d` produce synthetic RF data, usually through k-Wave. The PAM reconstruction loop consumes that RF data but does not call k-Wave directly. diff --git a/docs/src/troubleshooting.md b/docs/src/troubleshooting.md new file mode 100644 index 0000000..7b22b09 --- /dev/null +++ b/docs/src/troubleshooting.md @@ -0,0 +1,58 @@ +# Troubleshooting + +## The CLI Rejects My Argument + +Arguments must use `--name=value`. Space-separated forms such as `--name value` are not supported. + +For PAM, see [PAM CLI Parameters](@ref) for supported options. Unknown options are accepted by the low-level parser, but they only matter if the runner reads them. + +## CT Data Is Missing + +Skull-backed runs need private DICOM data. Use homogeneous water controls while testing setup: + +```bash +julia --project=. scripts/run_pam.jl \ + --source-model=point \ + --sources-mm=30:0 \ + --aberrator=none +``` + +For skull runs, pass the data location explicitly: + +```bash +--ct-path=/path/to/dicom-folder +``` + +## CUDA Is Not Available + +If CUDA.jl cannot see a GPU, set `--recon-use-gpu=false` for supported 2D runs. 3D reconstruction currently requires the CUDA path. + +k-Wave simulation GPU usage is controlled separately with `--kwave-use-gpu`. + +## k-Wave Or Python Setup Fails + +Python dependencies are managed through `CondaPkg.toml`. The first k-Wave run may take longer because Python packages and k-Wave resources are resolved. + +If setup fails, try instantiating the Julia environment first: + +```bash +julia --project=. -e 'using Pkg; Pkg.instantiate()' +``` + +Then run a homogeneous point-source example before attempting CT-backed skull simulations. + +## A Run Takes Too Long + +The largest runtime controls are grid size, dimensionality, reconstruction bandwidth, window count, and GPU usage. + +For faster iteration: + +- Start with `--aberrator=none`. +- Use `--source-model=point`. +- Reduce `--axial-mm` and `--transverse-mm`. +- Use tighter `--recon-bandwidth-khz`. +- Use `--recon-progress=true` to confirm reconstruction is moving. + +## Reconstruction-Only Reruns Fail + +`--from-run-dir` reuses the previous simulation data. Remove options that would change the simulation, source geometry, medium, or grid. Keep only reconstruction and analysis options such as `--recon-bandwidth-khz`, `--recon-step-um`, `--recon-use-gpu`, and threshold settings. diff --git a/docs/src/validation.md b/docs/src/validation.md new file mode 100644 index 0000000..7e6613b --- /dev/null +++ b/docs/src/validation.md @@ -0,0 +1,35 @@ +# Validation + +Validation scripts live under `validation/`. They are separate from the standard unit test suite and are intended to reproduce or sanity-check larger workflows. + +## 2D PAM Accuracy + +`validation/2D_PAM_Accuracy/run_validation.jl` reproduces a point-source localization sweep inspired by Schoen and Arvanitis. It simulates a grid of sources through a skull medium with k-Wave and reconstructs with homogeneous ASA and heterogeneous HASA. + +```bash +julia --project=. validation/2D_PAM_Accuracy/run_validation.jl +``` + +Use GPU reconstruction with: + +```bash +julia --project=. validation/2D_PAM_Accuracy/run_validation.jl --recon-use-gpu +``` + +## 3D PAM Accuracy + +`validation/3D_PAM_Accuracy/run_validation.jl` runs the 3D validation workflow. It is heavier than the unit tests and assumes the runtime environment has the required GPU and data dependencies. + +```bash +julia --project=. validation/3D_PAM_Accuracy/run_validation.jl +``` + +## Tests Versus Validation + +The standard test suite checks implementation behavior and smoke paths: + +```bash +julia --project=. -e 'using Pkg; Pkg.test()' +``` + +Validation scripts are larger reproducibility runs. They may require private CT data, a CUDA-capable GPU, and longer wall time. diff --git a/docs/src/workflow.md b/docs/src/workflow.md new file mode 100644 index 0000000..9ef50ae --- /dev/null +++ b/docs/src/workflow.md @@ -0,0 +1,56 @@ +# High-Level Workflow + +The repository is designed around script-level workflows. A typical PAM run goes through the same stages regardless of whether it is 2D or 3D. + +```mermaid +flowchart TD + userCLI["User runs CLI script"] --> parseOptions["Parse options"] + parseOptions --> sourceSetup["Build source model"] + parseOptions --> mediumSetup["Build medium"] + sourceSetup --> forwardSim["Run k-Wave or analytic simulation"] + mediumSetup --> forwardSim + forwardSim --> rfData["RF receiver data"] + rfData --> reconstruction["ASA or HASA reconstruction"] + mediumSetup --> reconstruction + reconstruction --> analysis["Metrics and figures"] + analysis --> outputs["summary.json, result.jld2, PNG outputs"] +``` + +## Source Setup + +The runner first turns CLI coordinates into source objects: + +- `point` sources are explicit tone-burst emitters. +- `squiggle` sources expand each anchor into a sampled vascular-like centerline. +- `network` sources grow a random branching 3D centerline structure and sample emitters inside an ellipsoid. + +Point sources are best for localization tests. Squiggle and network sources are intended for activity mapping and thresholded detection analysis. + +## Medium Setup + +For `--aberrator=none`, the medium is homogeneous water. For `--aberrator=skull`, the runner loads CT data, creates a skull/lens medium, and places it below the receiver plane according to `--skull-transducer-distance-mm`. + +Source coordinates remain defined relative to the receiver/transducer plane, not relative to the skull surface. + +## Forward Simulation + +The forward model produces RF pressure traces at the receiver plane. The maintained backend is k-Wave through the Julia/Python bridge. Some homogeneous cases can use the analytic backend. + +`--kwave-use-gpu` controls the k-Wave backend. This is separate from reconstruction GPU usage. + +## Reconstruction + +PAM reconstruction back-propagates the recorded RF data into the image domain: + +- Geometric ASA uses a homogeneous propagation model. +- HASA includes a heterogeneous correction term derived from the sound-speed field. +- Full reconstruction processes the full RF record at once. +- Windowed reconstruction partitions RF data into overlapping time windows and accumulates incoherent intensity. + +`--recon-mode=auto` uses full reconstruction for point sources and windowed reconstruction for squiggle/network activity. + +## Analysis + +Point-source runs report localization-style metrics such as peak location and error. Activity runs report thresholded detection metrics such as precision, recall, F1, false-positive area, and false-negative area. + +The complete run configuration and summary metrics are written to `summary.json`; numerical arrays and intermediate data are written to `result.jld2`. diff --git a/runs/20260506_103348_pam_aperture_sweep/01_aperture_ap50_dropout0p0_seed42/run.log b/runs/20260506_103348_pam_aperture_sweep/01_aperture_ap50_dropout0p0_seed42/run.log deleted file mode 100644 index 15c6790..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/01_aperture_ap50_dropout0p0_seed42/run.log +++ /dev/null @@ -1,81 +0,0 @@ -# 2026-05-06T10:33:49.067 -# `/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/01_aperture_ap50_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=50 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle` -WARNING:root:Highest prime factors in each dimension are [11 23] -WARNING:root:Use dimension sizes with lower prime factors to improve speed -┌───────────────────────────────────────────────────────────────┐ -│ kspaceFirstOrder-OMP v1.3 │ -├───────────────────────────────────────────────────────────────┤ -│ Reading simulation configuration: Done │ -│ Number of CPU threads: 12 │ -│ Processor name: │ -├───────────────────────────────────────────────────────────────┤ -│ Simulation details │ -├───────────────────────────────────────────────────────────────┤ -│ Domain dimensions: 440 x 552 │ -│ Medium type: 2D │ -│ Simulation time steps: 25000 │ -├───────────────────────────────────────────────────────────────┤ -│ Initialization │ -├───────────────────────────────────────────────────────────────┤ -│ Memory allocation: Done │ -│ Data loading: Done │ -│ Elapsed time: 0.01s │ -├───────────────────────────────────────────────────────────────┤ -│ FFT plans creation: Done │ -│ Pre-processing phase: Done │ -│ Elapsed time: 0.02s │ -├───────────────────────────────────────────────────────────────┤ -│ Computational resources │ -├───────────────────────────────────────────────────────────────┤ -│ Current host memory in use: 46758188462MB │ -│ Expected output file size: 23MB │ -├───────────────────────────────────────────────────────────────┤ -│ Simulation │ -├──────────┬────────────────┬──────────────┬────────────────────┤ -│ Progress │ Elapsed time │ Time to go │ Est. finish time │ -├──────────┼────────────────┼──────────────┼────────────────────┤ -│ 0% │ 0.010s │ 126.426s │ 06/05/26 10:36:29 │ -│ 5% │ 5.039s │ 95.589s │ 06/05/26 10:36:03 │ -│ 10% │ 9.987s │ 89.799s │ 06/05/26 10:36:02 │ -│ 15% │ 14.958s │ 84.707s │ 06/05/26 10:36:02 │ -│ 20% │ 20.258s │ 80.993s │ 06/05/26 10:36:03 │ -│ 25% │ 25.152s │ 75.423s │ 06/05/26 10:36:03 │ -│ 30% │ 30.026s │ 70.035s │ 06/05/26 10:36:03 │ -│ 35% │ 34.893s │ 64.778s │ 06/05/26 10:36:02 │ -│ 40% │ 39.760s │ 59.619s │ 06/05/26 10:36:01 │ -│ 45% │ 44.650s │ 54.554s │ 06/05/26 10:36:01 │ -│ 50% │ 49.545s │ 49.529s │ 06/05/26 10:36:01 │ -│ 55% │ 54.417s │ 44.509s │ 06/05/26 10:36:01 │ -│ 60% │ 59.292s │ 39.515s │ 06/05/26 10:36:01 │ -│ 65% │ 64.173s │ 34.543s │ 06/05/26 10:36:01 │ -│ 70% │ 69.051s │ 29.582s │ 06/05/26 10:36:01 │ -│ 75% │ 74.236s │ 24.735s │ 06/05/26 10:36:01 │ -│ 80% │ 79.072s │ 19.758s │ 06/05/26 10:36:01 │ -│ 85% │ 83.977s │ 14.810s │ 06/05/26 10:36:01 │ -│ 90% │ 88.792s │ 9.857s │ 06/05/26 10:36:01 │ -│ 95% │ 93.536s │ 4.915s │ 06/05/26 10:36:00 │ -├──────────┴────────────────┴──────────────┴────────────────────┤ -│ Elapsed time: 98.25s │ -├───────────────────────────────────────────────────────────────┤ -│ Sampled data post-processing: Done │ -│ Elapsed time: 0.00s │ -├───────────────────────────────────────────────────────────────┤ -│ Summary │ -├───────────────────────────────────────────────────────────────┤ -│ Peak memory in use: 6134162360MB │ -├───────────────────────────────────────────────────────────────┤ -│ Total execution time: 98.30s │ -├───────────────────────────────────────────────────────────────┤ -│ End of computation │ -└───────────────────────────────────────────────────────────────┘ - Activating project at `~/INI_code/Julia II` -┌ Info: reading DICOM series -└ dicom_dir = "/Users/vm/INI_code/Ultrasound/DIRU_20240404_human_skull_phase_correction_1_2_(skull_Normal)/DICOM/PAT_0000/STD_0000/SER_0002/OBJ_0001" -┌ Info: cropping ROI -│ index_xyz = (170, 190, 400) -└ size_xyz = (705, 360, 450) -┌ Info: resampling x/y only -│ spacing_x_mm = 0.201171875 -│ spacing_y_mm = 0.201171875 -└ new_spacing_xy_mm = 0.2 -Saved PAM cluster outputs to /Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/01_aperture_ap50_dropout0p0_seed42/run diff --git a/runs/20260506_103348_pam_aperture_sweep/01_aperture_ap50_dropout0p0_seed42/run/activity_boundaries.png b/runs/20260506_103348_pam_aperture_sweep/01_aperture_ap50_dropout0p0_seed42/run/activity_boundaries.png deleted file mode 100644 index 5a3db13..0000000 Binary files a/runs/20260506_103348_pam_aperture_sweep/01_aperture_ap50_dropout0p0_seed42/run/activity_boundaries.png and /dev/null differ diff --git a/runs/20260506_103348_pam_aperture_sweep/01_aperture_ap50_dropout0p0_seed42/run/overview.png b/runs/20260506_103348_pam_aperture_sweep/01_aperture_ap50_dropout0p0_seed42/run/overview.png deleted file mode 100644 index 3c16db1..0000000 Binary files a/runs/20260506_103348_pam_aperture_sweep/01_aperture_ap50_dropout0p0_seed42/run/overview.png and /dev/null differ diff --git a/runs/20260506_103348_pam_aperture_sweep/01_aperture_ap50_dropout0p0_seed42/run/summary.json b/runs/20260506_103348_pam_aperture_sweep/01_aperture_ap50_dropout0p0_seed42/run/summary.json deleted file mode 100644 index a81fab4..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/01_aperture_ap50_dropout0p0_seed42/run/summary.json +++ /dev/null @@ -1,2622 +0,0 @@ -{ - "reconstruction_bandwidth_hz": 500000, - "reconstruction_source": { - "mode": "simulation" - }, - "clusters": [ - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0060327725038961015, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -320.691671411612, - -481.03750711741804, - -641.383342823224 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.046321440287638786 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.005272737898667883, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -320.2235738069466, - -480.33536071041993, - -640.4471476138932 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04626569514702229 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0047312694272838695, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -317.8032279388882, - -476.70484190833236, - -635.6064558777764 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04572226666640846 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.004200866487451935, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -315.3467548948123, - -473.0201323422185, - -630.6935097896246 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04516619536230458 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0036565083902456493, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -312.9314759402867, - -469.39721391043014, - -625.8629518805734 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04461734980426383 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0031861889638953003, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -310.20409603501, - -465.3061440525151, - -620.40819207002 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04398719954353102 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.00256652716942303, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -308.0188633679521, - -462.0282950519281, - -616.0377267359042 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04348928418531734 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0017486279796490286, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -308.1907975338834, - -462.2861963008251, - -616.3815950677669 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04355435089147969 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.001125078113947233, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -309.8379271124607, - -464.75689066869114, - -619.6758542249214 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.043959799624315364 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0006028161597729909, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -312.11941106984887, - -468.17911660477336, - -624.2388221396977 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04451058220522799 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -2.401284689886321e-5, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -314.2460812377018, - -471.3691218565527, - -628.4921624754036 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.045020721921306135 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0004173119265126402, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -316.95746044523713, - -475.4361906678557, - -633.9149208904743 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04566686911546983 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0011125489663053104, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -318.6137903276162, - -477.9206854914243, - -637.2275806552324 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04605530266618959 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0017115638060362216, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -321.1407190707501, - -481.7110786061252, - -642.2814381415002 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04664759181635218 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0024823400105268, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -319.9942647843374, - -479.99139717650615, - -639.9885295686748 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.0463526618538167 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0031517892589508424, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -318.5017269206361, - -477.7525903809542, - -637.0034538412722 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04597133599677839 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.003865963263819151, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -316.9332073482421, - -475.3998110223631, - -633.8664146964842 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04556339958483016 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0043121080495665395, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -314.38452223900595, - -471.576783358509, - -628.7690444780119 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04492980070620905 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.004534541578681507, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -310.9262770322773, - -466.389415548416, - -621.8525540645546 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.044089545625208976 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.005237053357696269, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -309.35567932143914, - -464.03351898215874, - -618.7113586428783 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04366731027665283 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.005975114360697463, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -308.32565851717015, - -462.4884877757553, - -616.6513170343403 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.0433644115625137 - } - ], - "source_phase_mode": "random_phase_per_window", - "reconstruction_axial_step_m": 5.0e-5, - "detection_truth_mode": "centerline_tube", - "medium": { - "outer_row": 151, - "receiver_row": 1, - "hu_bone_thr": 200, - "slice_index": 250, - "inner_row": 186, - "aberrator": "skull", - "skull_to_transducer": 0.03, - "ct_path": "/Users/vm/INI_code/Ultrasound/DIRU_20240404_human_skull_phase_correction_1_2_(skull_Normal)/DICOM/PAT_0000/STD_0000/SER_0002/OBJ_0001" - }, - "analysis_mode": "detection", - "detection_threshold_ratio": 0.2, - "clean_threshold_ratio": 0.01, - "physical_source_count": 21, - "reconstruction_mode": "windowed", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/01_aperture_ap50_dropout0p0_seed42/run", - "window_info": { - "geometric": { - "effective_window_duration_s": 2.0e-5, - "used_window_ranges": [ - [ - 501, - 1500 - ], - [ - 1001, - 2000 - ], - [ - 1501, - 2500 - ], - [ - 2001, - 3000 - ], - [ - 2501, - 3500 - ], - [ - 3001, - 4000 - ], - [ - 3501, - 4500 - ], - [ - 4001, - 5000 - ], - [ - 4501, - 5500 - ], - [ - 5001, - 6000 - ], - [ - 5501, - 6500 - ], - [ - 6001, - 7000 - ], - [ - 6501, - 7500 - ], - [ - 7001, - 8000 - ], - [ - 7501, - 8500 - ], - [ - 8001, - 9000 - ], - [ - 8501, - 9500 - ], - [ - 9001, - 10000 - ], - [ - 9501, - 10500 - ], - [ - 10001, - 11000 - ], - [ - 10501, - 11500 - ], - [ - 11001, - 12000 - ], - [ - 11501, - 12500 - ], - [ - 12001, - 13000 - ], - [ - 12501, - 13500 - ], - [ - 13001, - 14000 - ], - [ - 13501, - 14500 - ], - [ - 14001, - 15000 - ], - [ - 14501, - 15500 - ], - [ - 15001, - 16000 - ], - [ - 15501, - 16500 - ], - [ - 16001, - 17000 - ], - [ - 16501, - 17500 - ], - [ - 17001, - 18000 - ], - [ - 17501, - 18500 - ], - [ - 18001, - 19000 - ], - [ - 18501, - 19500 - ], - [ - 19001, - 20000 - ], - [ - 19501, - 20500 - ], - [ - 20001, - 21000 - ], - [ - 20501, - 21500 - ], - [ - 21001, - 22000 - ], - [ - 21501, - 22500 - ], - [ - 22001, - 23000 - ], - [ - 22501, - 23500 - ], - [ - 23001, - 24000 - ], - [ - 23501, - 24500 - ], - [ - 24001, - 25000 - ] - ], - "used_window_count": 48, - "skipped_window_ranges": [ - [ - 1, - 1000 - ] - ], - "window_samples": 1000, - "effective_hop_s": 1.0e-5, - "energy_threshold": 384.0531655369217, - "skipped_window_count": 1, - "accumulation": "intensity", - "total_window_count": 49, - "hop_samples": 500 - }, - "hasa": { - "effective_window_duration_s": 2.0e-5, - "used_window_ranges": [ - [ - 501, - 1500 - ], - [ - 1001, - 2000 - ], - [ - 1501, - 2500 - ], - [ - 2001, - 3000 - ], - [ - 2501, - 3500 - ], - [ - 3001, - 4000 - ], - [ - 3501, - 4500 - ], - [ - 4001, - 5000 - ], - [ - 4501, - 5500 - ], - [ - 5001, - 6000 - ], - [ - 5501, - 6500 - ], - [ - 6001, - 7000 - ], - [ - 6501, - 7500 - ], - [ - 7001, - 8000 - ], - [ - 7501, - 8500 - ], - [ - 8001, - 9000 - ], - [ - 8501, - 9500 - ], - [ - 9001, - 10000 - ], - [ - 9501, - 10500 - ], - [ - 10001, - 11000 - ], - [ - 10501, - 11500 - ], - [ - 11001, - 12000 - ], - [ - 11501, - 12500 - ], - [ - 12001, - 13000 - ], - [ - 12501, - 13500 - ], - [ - 13001, - 14000 - ], - [ - 13501, - 14500 - ], - [ - 14001, - 15000 - ], - [ - 14501, - 15500 - ], - [ - 15001, - 16000 - ], - [ - 15501, - 16500 - ], - [ - 16001, - 17000 - ], - [ - 16501, - 17500 - ], - [ - 17001, - 18000 - ], - [ - 17501, - 18500 - ], - [ - 18001, - 19000 - ], - [ - 18501, - 19500 - ], - [ - 19001, - 20000 - ], - [ - 19501, - 20500 - ], - [ - 20001, - 21000 - ], - [ - 20501, - 21500 - ], - [ - 21001, - 22000 - ], - [ - 21501, - 22500 - ], - [ - 22001, - 23000 - ], - [ - 22501, - 23500 - ], - [ - 23001, - 24000 - ], - [ - 23501, - 24500 - ], - [ - 24001, - 25000 - ] - ], - "used_window_count": 48, - "skipped_window_ranges": [ - [ - 1, - 1000 - ] - ], - "window_samples": 1000, - "effective_hop_s": 1.0e-5, - "energy_threshold": 384.0531655369217, - "skipped_window_count": 1, - "accumulation": "intensity", - "total_window_count": 49, - "hop_samples": 500 - } - }, - "window_config": { - "window_duration_s": 1.9999999999999998e-5, - "enabled": true, - "hop_s": 9.999999999999999e-6, - "min_energy_ratio": 0.001, - "taper": "hann", - "accumulation": "intensity" - }, - "activity_boundary_figure": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/01_aperture_ap50_dropout0p0_seed42/run/activity_boundaries.png", - "reference_sound_speed_m_per_s": 1551.0647917287392, - "emission_event_count": 21, - "hasa": { - "truth_components": 1, - "true_negative_pixels": 180994, - "energy_outside_predicted_mask": 1.2160141405400944e10, - "centroid_depth_mm": 45.50438583563683, - "energy_total": 3.222824694406598e10, - "false_negative_area_mm2": 1.08, - "centroid_error_mm": 0.9850847735466974, - "target_axial_spread_mm": 1.3028997817899934, - "prediction_components": 3, - "false_negative_pixels": 27, - "psf_target_ssim_like": 0.26321673225208475, - "matched_prediction_components": 1, - "recall": 0.968421052631579, - "f1": 0.06722416172769342, - "energy_fraction_outside_predicted_mask": 0.3773131509916002, - "psf_target_mode": "provided_mask", - "energy_outside_mask": 3.105834942496367e10, - "energy_fraction_inside_predicted_mask": 0.6226868490083998, - "axial_spread_mm": 22.069387711062245, - "false_positive_area_mm2": 918.04, - "missed_truth_components": 0, - "psf_target_normalized_l2_error": 0.967300772825553, - "psf_target_correlation": 0.2917420463453979, - "true_positive_pixels": 828, - "target_centroid_depth_mm": 44.999999999999915, - "energy_fraction_outside_mask": 0.9636996228453649, - "num_truth_sources": 21, - "recovered_truth_components": 1, - "target_centroid_lateral_mm": -3.6804216213733665e-19, - "psf_axial_fwhm_mm": 0.75, - "energy_inside_mask": 1.16989751910231e9, - "precision": 0.034820640060557635, - "dice": 0.06722416172769344, - "jaccard": 0.034781147609846255, - "centroid_lateral_mm": -0.846160114802392, - "peak_mm": [ - 47.4, - -4.6 - ], - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "predicted_area_mm2": 951.16, - "overlap_area_mm2": 33.12, - "max_intensity": 2.1970094036380644e6, - "threshold_ratio": 0.2, - "false_positive_pixels": 22951, - "threshold_db": -6.9897000433601875, - "truth_mask_mode": "provided", - "energy_inside_predicted_mask": 2.0068105538665035e10, - "truth_radius_mm": 1, - "energy_fraction_inside_mask": 0.03630037715463506, - "psf_target_integral": 854.9999999999999, - "truth_area_mm2": 34.2, - "spurious_prediction_components": 2, - "target_lateral_spread_mm": 3.8127694595420865, - "psf_lateral_fwhm_mm": 0.44999999999999996, - "lateral_spread_mm": 9.331526523807156, - "peak_intensity": 2.1970094036380644e6 - }, - "config": { - "axial_dim": 0.08, - "dt": 2.0e-8, - "bottom_margin": 0.01, - "rho0": 1000, - "success_tolerance": 0.0015, - "dx": 0.0002, - "receiver_aperture": 0.05, - "dz": 0.0002, - "zero_pad_factor": 4, - "transverse_dim": 0.1024, - "t_max": 0.0005, - "c0": 1500, - "peak_suppression_radius": 0.008 - }, - "peak_method": "argmax", - "clean_loop_gain": 0.1, - "geometric": { - "truth_components": 1, - "true_negative_pixels": 162097, - "energy_outside_predicted_mask": 4.35705512114312e9, - "centroid_depth_mm": 39.984398371080154, - "energy_total": 2.160146744086769e10, - "false_negative_area_mm2": 0, - "centroid_error_mm": 5.015617813618699, - "target_axial_spread_mm": 1.3028997817899934, - "prediction_components": 2, - "false_negative_pixels": 0, - "psf_target_ssim_like": 0.24637032608151105, - "matched_prediction_components": 1, - "recall": 1, - "f1": 0.03925800082648423, - "energy_fraction_outside_predicted_mask": 0.20170181183617333, - "psf_target_mode": "provided_mask", - "energy_outside_mask": 2.1019082240380314e10, - "energy_fraction_inside_predicted_mask": 0.7982981881638267, - "axial_spread_mm": 23.033017216387158, - "false_positive_area_mm2": 1673.92, - "missed_truth_components": 0, - "psf_target_normalized_l2_error": 0.977073974795333, - "psf_target_correlation": 0.2293872621287592, - "true_positive_pixels": 855, - "target_centroid_depth_mm": 44.999999999999915, - "energy_fraction_outside_mask": 0.9730395538135725, - "num_truth_sources": 21, - "recovered_truth_components": 1, - "target_centroid_lateral_mm": -3.6804216213733665e-19, - "psf_axial_fwhm_mm": 0.75, - "energy_inside_mask": 5.823852004873776e8, - "precision": 0.02002201250497623, - "dice": 0.03925800082648423, - "jaccard": 0.02002201250497623, - "centroid_lateral_mm": 0.012741752888224985, - "peak_mm": [ - 35.2, - 0.4 - ], - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "predicted_area_mm2": 1708.1200000000001, - "overlap_area_mm2": 34.2, - "max_intensity": 1.1120660380130445e6, - "threshold_ratio": 0.2, - "false_positive_pixels": 41848, - "threshold_db": -6.9897000433601875, - "truth_mask_mode": "provided", - "energy_inside_predicted_mask": 1.724441231972457e10, - "truth_radius_mm": 1, - "energy_fraction_inside_mask": 0.02696044618642743, - "psf_target_integral": 854.9999999999999, - "truth_area_mm2": 34.2, - "spurious_prediction_components": 1, - "target_lateral_spread_mm": 3.8127694595420865, - "psf_lateral_fwhm_mm": 0.44999999999999996, - "lateral_spread_mm": 9.556075061984597, - "peak_intensity": 1.1120660380130445e6 - }, - "clean_max_iter": 500, - "source_variability": { - "amplitude_distribution": "fixed", - "frequency_jitter_percent": 1, - "amplitude_sigma": 0, - "dropout_probability": 0 - }, - "emission_meta": { - "emission_event_count": 21, - "random_seed": 42, - "harmonics": [ - 2, - 3, - 4 - ], - "phase_jitter_rad": 0.2, - "n_bubbles_per_cluster": [ - 10 - ], - "cluster_model": "vascular", - "phase_mode": "geometric", - "n_emission_sources": 21, - "cavitation_model": "harmonic_cos", - "gate_duration_s": 4.9999999999999996e-5, - "delays_s": [ - 0 - ], - "activity_model": { - "emission_event_count": 21, - "activity_mode": "static", - "phase_jitter_rad": 0, - "active_probability": 1, - "physical_source_count": 21, - "hop_s": null, - "amplitude_jitter": 0, - "frame_duration_s": null - }, - "transducer_m": [ - -0.03, - 0.0 - ], - "fundamental_hz": 500000, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "vascular": { - "branch_levels": 2, - "source_spacing_m": 0.0008, - "bundle_count": 3, - "centerlines_m": [ - [ - [ - 0.0465, - -0.006 - ], - [ - 0.046426584774442725, - -0.0056 - ], - [ - 0.04621352549156242, - -0.0052 - ], - [ - 0.045881677878438706, - -0.0048 - ], - [ - 0.04546352549156242, - -0.0044 - ], - [ - 0.045, - -0.004 - ], - [ - 0.044536474508437576, - -0.0036 - ], - [ - 0.04411832212156129, - -0.0032 - ], - [ - 0.043786474508437576, - -0.0028 - ], - [ - 0.043573415225557265, - -0.0024 - ], - [ - 0.0435, - -0.002 - ], - [ - 0.04357341522555727, - -0.0016 - ], - [ - 0.043786474508437576, - -0.0012 - ], - [ - 0.04411832212156129, - -0.0008 - ], - [ - 0.044536474508437576, - -0.0004 - ], - [ - 0.045, - 0.0 - ], - [ - 0.04546352549156242, - 0.0004 - ], - [ - 0.045881677878438706, - 0.0008 - ], - [ - 0.04621352549156242, - 0.0012 - ], - [ - 0.046426584774442725, - 0.0016 - ], - [ - 0.0465, - 0.002 - ], - [ - 0.04642658477444273, - 0.0024 - ], - [ - 0.04621352549156242, - 0.0028 - ], - [ - 0.045881677878438706, - 0.0032 - ], - [ - 0.04546352549156242, - 0.0036 - ], - [ - 0.045, - 0.004 - ], - [ - 0.044536474508437576, - 0.0044 - ], - [ - 0.04411832212156129, - 0.0048 - ], - [ - 0.043786474508437576, - 0.0052 - ], - [ - 0.04357341522555727, - 0.0056 - ], - [ - 0.0435, - 0.006 - ] - ] - ], - "min_separation_m": 0.0003, - "branch_angle_rad": 0.5235987755982988, - "position_jitter_m": 0.00015, - "squiggle_amplitude_m": 0.0015, - "anchors": [ - { - "segments_m": [ - ], - "anchor_m": [ - 0.045, - 0.0 - ], - "source_count": 21, - "centerlines_m": [ - [ - [ - 0.0465, - -0.006 - ], - [ - 0.046426584774442725, - -0.0056 - ], - [ - 0.04621352549156242, - -0.0052 - ], - [ - 0.045881677878438706, - -0.0048 - ], - [ - 0.04546352549156242, - -0.0044 - ], - [ - 0.045, - -0.004 - ], - [ - 0.044536474508437576, - -0.0036 - ], - [ - 0.04411832212156129, - -0.0032 - ], - [ - 0.043786474508437576, - -0.0028 - ], - [ - 0.043573415225557265, - -0.0024 - ], - [ - 0.0435, - -0.002 - ], - [ - 0.04357341522555727, - -0.0016 - ], - [ - 0.043786474508437576, - -0.0012 - ], - [ - 0.04411832212156129, - -0.0008 - ], - [ - 0.044536474508437576, - -0.0004 - ], - [ - 0.045, - 0.0 - ], - [ - 0.04546352549156242, - 0.0004 - ], - [ - 0.045881677878438706, - 0.0008 - ], - [ - 0.04621352549156242, - 0.0012 - ], - [ - 0.046426584774442725, - 0.0016 - ], - [ - 0.0465, - 0.002 - ], - [ - 0.04642658477444273, - 0.0024 - ], - [ - 0.04621352549156242, - 0.0028 - ], - [ - 0.045881677878438706, - 0.0032 - ], - [ - 0.04546352549156242, - 0.0036 - ], - [ - 0.045, - 0.004 - ], - [ - 0.044536474508437576, - 0.0044 - ], - [ - 0.04411832212156129, - 0.0048 - ], - [ - 0.043786474508437576, - 0.0052 - ], - [ - 0.04357341522555727, - 0.0056 - ], - [ - 0.0435, - 0.006 - ] - ] - ] - } - ], - "branch_scale": 0.65, - "topology": "squiggle", - "max_sources_per_anchor": 0, - "truth_radius_m": 0.001, - "squiggle_wavelength_m": 0.008, - "length_m": 0.012, - "squiggle_slope": 0, - "bundle_spacing_m": 0.002 - }, - "n_anchor_clusters": 1, - "physical_source_count": 21, - "anchor_clusters_m": [ - [ - 0.045, - 0.0 - ] - ] - }, - "n_realizations": 1, - "activity_model": { - "emission_event_count": 21, - "activity_mode": "static", - "phase_jitter_rad": 0, - "active_probability": 1, - "physical_source_count": 21, - "hop_s": null, - "amplitude_jitter": 0, - "frame_duration_s": null - }, - "detection_truth_radius_m": 0.001, - "boundary_threshold_ratios": [ - 0.6, - 0.65, - 0.7 - ], - "simulation": { - "receiver_row": 1, - "source_indices": [ - [ - 233, - 227 - ], - [ - 232, - 231 - ], - [ - 230, - 233 - ], - [ - 227, - 236 - ], - [ - 224, - 239 - ], - [ - 221, - 241 - ], - [ - 218, - 244 - ], - [ - 219, - 248 - ], - [ - 221, - 251 - ], - [ - 224, - 254 - ], - [ - 226, - 257 - ], - [ - 229, - 259 - ], - [ - 231, - 263 - ], - [ - 234, - 266 - ], - [ - 233, - 269 - ], - [ - 231, - 273 - ], - [ - 229, - 276 - ], - [ - 226, - 279 - ], - [ - 221, - 280 - ], - [ - 219, - 283 - ], - [ - 218, - 287 - ] - ], - "receiver_cols": [ - 132, - 381 - ] - }, - "reconstruction_frequencies_hz": [ - 1000000, - 1500000, - 2000000 - ], - "activity_boundary_metrics": { - "geometric": [ - { - "target_centroid_lateral_mm": -3.6804216213733665e-19, - "axial_spread_mm": 23.033017216387158, - "peak_intensity": 1.1120660380130445e6, - "dice": 0.19970845481049562, - "false_positive_area_mm2": 163.4, - "energy_inside_predicted_mask": 3.5808849899908175e9, - "overlap_area_mm2": 21.92, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.8342295494603222, - "centroid_lateral_mm": 0.012741752888224985, - "f1": 0.19970845481049565, - "threshold_ratio": 0.6, - "false_positive_pixels": 4085, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.44999999999999996, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.999999999999915, - "true_negative_pixels": 199860, - "energy_outside_predicted_mask": 1.8020582450876873e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 2.160146744086769e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 4, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899934, - "jaccard": 0.11093117408906883, - "psf_target_correlation": 0.2293872621287592, - "psf_target_ssim_like": 0.24637032608151105, - "precision": 0.1182818907835096, - "peak_mm": [ - 35.2, - 0.4 - ], - "energy_fraction_inside_predicted_mask": 0.1657704505396778, - "recovered_truth_components": 1, - "lateral_spread_mm": 9.556075061984597, - "true_positive_pixels": 548, - "centroid_error_mm": 5.015617813618699, - "recall": 0.6409356725146199, - "matched_prediction_components": 1, - "prediction_components": 5, - "false_negative_pixels": 307, - "energy_fraction_inside_mask": 0.02696044618642743, - "predicted_area_mm2": 185.32, - "energy_fraction_outside_mask": 0.9730395538135725, - "max_intensity": 1.1120660380130445e6, - "energy_inside_mask": 5.823852004873776e8, - "centroid_depth_mm": 39.984398371080154, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8127694595420865, - "psf_target_normalized_l2_error": 0.977073974795333, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 12.280000000000001, - "energy_outside_mask": 2.1019082240380314e10, - "num_truth_sources": 21, - "threshold_db": -2.218487496163564 - }, - { - "target_centroid_lateral_mm": -3.6804216213733665e-19, - "axial_spread_mm": 23.033017216387158, - "peak_intensity": 1.1120660380130445e6, - "dice": 0.22765902652588205, - "false_positive_area_mm2": 103.44, - "energy_inside_predicted_mask": 2.468741993344998e9, - "overlap_area_mm2": 17.68, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.8857141534433721, - "centroid_lateral_mm": 0.012741752888224985, - "f1": 0.22765902652588207, - "threshold_ratio": 0.65, - "false_positive_pixels": 2586, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.44999999999999996, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.999999999999915, - "true_negative_pixels": 201359, - "energy_outside_predicted_mask": 1.9132725447522694e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 2.160146744086769e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 1, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899934, - "jaccard": 0.12845103167683813, - "psf_target_correlation": 0.2293872621287592, - "psf_target_ssim_like": 0.24637032608151105, - "precision": 0.14597093791281374, - "peak_mm": [ - 35.2, - 0.4 - ], - "energy_fraction_inside_predicted_mask": 0.11428584655662787, - "recovered_truth_components": 1, - "lateral_spread_mm": 9.556075061984597, - "true_positive_pixels": 442, - "centroid_error_mm": 5.015617813618699, - "recall": 0.5169590643274854, - "matched_prediction_components": 1, - "prediction_components": 2, - "false_negative_pixels": 413, - "energy_fraction_inside_mask": 0.02696044618642743, - "predicted_area_mm2": 121.12, - "energy_fraction_outside_mask": 0.9730395538135725, - "max_intensity": 1.1120660380130445e6, - "energy_inside_mask": 5.823852004873776e8, - "centroid_depth_mm": 39.984398371080154, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8127694595420865, - "psf_target_normalized_l2_error": 0.977073974795333, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 16.52, - "energy_outside_mask": 2.1019082240380314e10, - "num_truth_sources": 21, - "threshold_db": -1.8708664335714442 - }, - { - "target_centroid_lateral_mm": -3.6804216213733665e-19, - "axial_spread_mm": 23.033017216387158, - "peak_intensity": 1.1120660380130445e6, - "dice": 0.23684210526315788, - "false_positive_area_mm2": 62.28, - "energy_inside_predicted_mask": 1.6101352557280283e9, - "overlap_area_mm2": 12.96, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.9254617650335263, - "centroid_lateral_mm": 0.012741752888224985, - "f1": 0.23684210526315785, - "threshold_ratio": 0.7, - "false_positive_pixels": 1557, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.44999999999999996, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.999999999999915, - "true_negative_pixels": 202388, - "energy_outside_predicted_mask": 1.9991332185139664e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 2.160146744086769e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 4, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899934, - "jaccard": 0.13432835820895522, - "psf_target_correlation": 0.2293872621287592, - "psf_target_ssim_like": 0.24637032608151105, - "precision": 0.1722488038277512, - "peak_mm": [ - 35.2, - 0.4 - ], - "energy_fraction_inside_predicted_mask": 0.07453823496647374, - "recovered_truth_components": 1, - "lateral_spread_mm": 9.556075061984597, - "true_positive_pixels": 324, - "centroid_error_mm": 5.015617813618699, - "recall": 0.37894736842105264, - "matched_prediction_components": 2, - "prediction_components": 6, - "false_negative_pixels": 531, - "energy_fraction_inside_mask": 0.02696044618642743, - "predicted_area_mm2": 75.24, - "energy_fraction_outside_mask": 0.9730395538135725, - "max_intensity": 1.1120660380130445e6, - "energy_inside_mask": 5.823852004873776e8, - "centroid_depth_mm": 39.984398371080154, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8127694595420865, - "psf_target_normalized_l2_error": 0.977073974795333, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 21.240000000000002, - "energy_outside_mask": 2.1019082240380314e10, - "num_truth_sources": 21, - "threshold_db": -1.5490195998574319 - } - ], - "hasa": [ - { - "target_centroid_lateral_mm": -3.6804216213733665e-19, - "axial_spread_mm": 22.069387711062245, - "peak_intensity": 2.1970094036380644e6, - "dice": 0.30932999373825926, - "false_positive_area_mm2": 73.8, - "energy_inside_predicted_mask": 3.591303367810137e9, - "overlap_area_mm2": 19.76, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.888566592714675, - "centroid_lateral_mm": -0.846160114802392, - "f1": 0.3093299937382592, - "threshold_ratio": 0.6, - "false_positive_pixels": 1845, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.44999999999999996, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.999999999999915, - "true_negative_pixels": 202100, - "energy_outside_predicted_mask": 2.8636943576255844e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 3.222824694406598e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 5, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899934, - "jaccard": 0.18296296296296297, - "psf_target_correlation": 0.2917420463453979, - "psf_target_ssim_like": 0.26321673225208475, - "precision": 0.21120136810602821, - "peak_mm": [ - 47.4, - -4.6 - ], - "energy_fraction_inside_predicted_mask": 0.11143340728532505, - "recovered_truth_components": 1, - "lateral_spread_mm": 9.331526523807156, - "true_positive_pixels": 494, - "centroid_error_mm": 0.9850847735466974, - "recall": 0.5777777777777777, - "matched_prediction_components": 2, - "prediction_components": 7, - "false_negative_pixels": 361, - "energy_fraction_inside_mask": 0.03630037715463506, - "predicted_area_mm2": 93.56, - "energy_fraction_outside_mask": 0.9636996228453649, - "max_intensity": 2.1970094036380644e6, - "energy_inside_mask": 1.16989751910231e9, - "centroid_depth_mm": 45.50438583563683, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8127694595420865, - "psf_target_normalized_l2_error": 0.967300772825553, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 14.44, - "energy_outside_mask": 3.105834942496367e10, - "num_truth_sources": 21, - "threshold_db": -2.218487496163564 - }, - { - "target_centroid_lateral_mm": -3.6804216213733665e-19, - "axial_spread_mm": 22.069387711062245, - "peak_intensity": 2.1970094036380644e6, - "dice": 0.35970561177552896, - "false_positive_area_mm2": 37.12, - "energy_inside_predicted_mask": 2.2016289271673765e9, - "overlap_area_mm2": 15.64, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.9316863579024812, - "centroid_lateral_mm": -0.846160114802392, - "f1": 0.359705611775529, - "threshold_ratio": 0.65, - "false_positive_pixels": 928, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.44999999999999996, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.999999999999915, - "true_negative_pixels": 203017, - "energy_outside_predicted_mask": 3.00266180168986e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 3.222824694406598e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 2, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899934, - "jaccard": 0.21929332585530006, - "psf_target_correlation": 0.2917420463453979, - "psf_target_ssim_like": 0.26321673225208475, - "precision": 0.2964366944655042, - "peak_mm": [ - 47.4, - -4.6 - ], - "energy_fraction_inside_predicted_mask": 0.06831364209751878, - "recovered_truth_components": 1, - "lateral_spread_mm": 9.331526523807156, - "true_positive_pixels": 391, - "centroid_error_mm": 0.9850847735466974, - "recall": 0.45730994152046783, - "matched_prediction_components": 2, - "prediction_components": 4, - "false_negative_pixels": 464, - "energy_fraction_inside_mask": 0.03630037715463506, - "predicted_area_mm2": 52.76, - "energy_fraction_outside_mask": 0.9636996228453649, - "max_intensity": 2.1970094036380644e6, - "energy_inside_mask": 1.16989751910231e9, - "centroid_depth_mm": 45.50438583563683, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8127694595420865, - "psf_target_normalized_l2_error": 0.967300772825553, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 18.56, - "energy_outside_mask": 3.105834942496367e10, - "num_truth_sources": 21, - "threshold_db": -1.8708664335714442 - }, - { - "target_centroid_lateral_mm": -3.6804216213733665e-19, - "axial_spread_mm": 22.069387711062245, - "peak_intensity": 2.1970094036380644e6, - "dice": 0.3213885778275476, - "false_positive_area_mm2": 25.76, - "energy_inside_predicted_mask": 1.6274823594380238e9, - "overlap_area_mm2": 11.48, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.9495013687135198, - "centroid_lateral_mm": -0.846160114802392, - "f1": 0.3213885778275476, - "threshold_ratio": 0.7, - "false_positive_pixels": 644, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.44999999999999996, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.999999999999915, - "true_negative_pixels": 203301, - "energy_outside_predicted_mask": 3.0600764584627956e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 3.222824694406598e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 0, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899934, - "jaccard": 0.1914609739826551, - "psf_target_correlation": 0.2917420463453979, - "psf_target_ssim_like": 0.26321673225208475, - "precision": 0.3082706766917293, - "peak_mm": [ - 47.4, - -4.6 - ], - "energy_fraction_inside_predicted_mask": 0.050498631286480314, - "recovered_truth_components": 1, - "lateral_spread_mm": 9.331526523807156, - "true_positive_pixels": 287, - "centroid_error_mm": 0.9850847735466974, - "recall": 0.33567251461988307, - "matched_prediction_components": 2, - "prediction_components": 2, - "false_negative_pixels": 568, - "energy_fraction_inside_mask": 0.03630037715463506, - "predicted_area_mm2": 37.24, - "energy_fraction_outside_mask": 0.9636996228453649, - "max_intensity": 2.1970094036380644e6, - "energy_inside_mask": 1.16989751910231e9, - "centroid_depth_mm": 45.50438583563683, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8127694595420865, - "psf_target_normalized_l2_error": 0.967300772825553, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 22.72, - "energy_outside_mask": 3.105834942496367e10, - "num_truth_sources": 21, - "threshold_db": -1.5490195998574319 - } - ], - "threshold_ratios": [ - 0.6, - 0.65, - 0.7 - ] - } -} \ No newline at end of file diff --git a/runs/20260506_103348_pam_aperture_sweep/01_aperture_ap50_dropout0p0_seed42/status.json b/runs/20260506_103348_pam_aperture_sweep/01_aperture_ap50_dropout0p0_seed42/status.json deleted file mode 100644 index ef23671..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/01_aperture_ap50_dropout0p0_seed42/status.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "best_hasa_f1": 0.359705611775529, - "best_hasa_recall": 0.45730994152046783, - "hasa_psf_corr": 0.2917420463453979, - "best_geo_threshold": 0.7, - "hasa_centroid_error_mm": 0.9850847735466974, - "best_hasa_jaccard": 0.21929332585530006, - "summary_found": true, - "kind": "simulation", - "best_hasa_precision": 0.2964366944655042, - "status": "success", - "elapsed_min": 6.42, - "id": "aperture_ap50_dropout0p0_seed42", - "best_geo_f1": 0.23684210526315785, - "cmd": "`/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/01_aperture_ap50_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=50 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle`", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/01_aperture_ap50_dropout0p0_seed42/run", - "hasa_psf_l2": 0.967300772825553, - "best_hasa_threshold": 0.65 -} diff --git a/runs/20260506_103348_pam_aperture_sweep/02_aperture_ap60_dropout0p0_seed42/run.log b/runs/20260506_103348_pam_aperture_sweep/02_aperture_ap60_dropout0p0_seed42/run.log deleted file mode 100644 index 6d15119..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/02_aperture_ap60_dropout0p0_seed42/run.log +++ /dev/null @@ -1,81 +0,0 @@ -# 2026-05-06T10:40:14.364 -# `/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/02_aperture_ap60_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=60 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle` -WARNING:root:Highest prime factors in each dimension are [11 23] -WARNING:root:Use dimension sizes with lower prime factors to improve speed -┌───────────────────────────────────────────────────────────────┐ -│ kspaceFirstOrder-OMP v1.3 │ -├───────────────────────────────────────────────────────────────┤ -│ Reading simulation configuration: Done │ -│ Number of CPU threads: 12 │ -│ Processor name: │ -├───────────────────────────────────────────────────────────────┤ -│ Simulation details │ -├───────────────────────────────────────────────────────────────┤ -│ Domain dimensions: 440 x 552 │ -│ Medium type: 2D │ -│ Simulation time steps: 25000 │ -├───────────────────────────────────────────────────────────────┤ -│ Initialization │ -├───────────────────────────────────────────────────────────────┤ -│ Memory allocation: Done │ -│ Data loading: Done │ -│ Elapsed time: 0.01s │ -├───────────────────────────────────────────────────────────────┤ -│ FFT plans creation: Done │ -│ Pre-processing phase: Done │ -│ Elapsed time: 0.02s │ -├───────────────────────────────────────────────────────────────┤ -│ Computational resources │ -├───────────────────────────────────────────────────────────────┤ -│ Current host memory in use: 34275969966MB │ -│ Expected output file size: 28MB │ -├───────────────────────────────────────────────────────────────┤ -│ Simulation │ -├──────────┬────────────────┬──────────────┬────────────────────┤ -│ Progress │ Elapsed time │ Time to go │ Est. finish time │ -├──────────┼────────────────┼──────────────┼────────────────────┤ -│ 0% │ 0.010s │ 124.227s │ 06/05/26 10:42:55 │ -│ 5% │ 4.930s │ 93.513s │ 06/05/26 10:42:29 │ -│ 10% │ 9.815s │ 88.254s │ 06/05/26 10:42:28 │ -│ 15% │ 14.701s │ 83.256s │ 06/05/26 10:42:28 │ -│ 20% │ 19.596s │ 78.345s │ 06/05/26 10:42:28 │ -│ 25% │ 24.494s │ 73.451s │ 06/05/26 10:42:28 │ -│ 30% │ 29.382s │ 68.532s │ 06/05/26 10:42:28 │ -│ 35% │ 34.270s │ 63.621s │ 06/05/26 10:42:28 │ -│ 40% │ 39.202s │ 58.784s │ 06/05/26 10:42:28 │ -│ 45% │ 44.097s │ 53.879s │ 06/05/26 10:42:28 │ -│ 50% │ 48.988s │ 48.973s │ 06/05/26 10:42:28 │ -│ 55% │ 53.895s │ 44.082s │ 06/05/26 10:42:28 │ -│ 60% │ 58.774s │ 39.170s │ 06/05/26 10:42:28 │ -│ 65% │ 63.653s │ 34.263s │ 06/05/26 10:42:28 │ -│ 70% │ 68.544s │ 29.365s │ 06/05/26 10:42:28 │ -│ 75% │ 73.432s │ 24.467s │ 06/05/26 10:42:28 │ -│ 80% │ 78.319s │ 19.570s │ 06/05/26 10:42:28 │ -│ 85% │ 83.201s │ 14.673s │ 06/05/26 10:42:28 │ -│ 90% │ 88.082s │ 9.778s │ 06/05/26 10:42:28 │ -│ 95% │ 92.966s │ 4.885s │ 06/05/26 10:42:28 │ -├──────────┴────────────────┴──────────────┴────────────────────┤ -│ Elapsed time: 97.85s │ -├───────────────────────────────────────────────────────────────┤ -│ Sampled data post-processing: Done │ -│ Elapsed time: 0.00s │ -├───────────────────────────────────────────────────────────────┤ -│ Summary │ -├───────────────────────────────────────────────────────────────┤ -│ Peak memory in use: 6158230456MB │ -├───────────────────────────────────────────────────────────────┤ -│ Total execution time: 97.90s │ -├───────────────────────────────────────────────────────────────┤ -│ End of computation │ -└───────────────────────────────────────────────────────────────┘ - Activating project at `~/INI_code/Julia II` -┌ Info: reading DICOM series -└ dicom_dir = "/Users/vm/INI_code/Ultrasound/DIRU_20240404_human_skull_phase_correction_1_2_(skull_Normal)/DICOM/PAT_0000/STD_0000/SER_0002/OBJ_0001" -┌ Info: cropping ROI -│ index_xyz = (170, 190, 400) -└ size_xyz = (705, 360, 450) -┌ Info: resampling x/y only -│ spacing_x_mm = 0.201171875 -│ spacing_y_mm = 0.201171875 -└ new_spacing_xy_mm = 0.2 -Saved PAM cluster outputs to /Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/02_aperture_ap60_dropout0p0_seed42/run diff --git a/runs/20260506_103348_pam_aperture_sweep/02_aperture_ap60_dropout0p0_seed42/run/activity_boundaries.png b/runs/20260506_103348_pam_aperture_sweep/02_aperture_ap60_dropout0p0_seed42/run/activity_boundaries.png deleted file mode 100644 index cc6716f..0000000 Binary files a/runs/20260506_103348_pam_aperture_sweep/02_aperture_ap60_dropout0p0_seed42/run/activity_boundaries.png and /dev/null differ diff --git a/runs/20260506_103348_pam_aperture_sweep/02_aperture_ap60_dropout0p0_seed42/run/overview.png b/runs/20260506_103348_pam_aperture_sweep/02_aperture_ap60_dropout0p0_seed42/run/overview.png deleted file mode 100644 index ca09439..0000000 Binary files a/runs/20260506_103348_pam_aperture_sweep/02_aperture_ap60_dropout0p0_seed42/run/overview.png and /dev/null differ diff --git a/runs/20260506_103348_pam_aperture_sweep/02_aperture_ap60_dropout0p0_seed42/run/summary.json b/runs/20260506_103348_pam_aperture_sweep/02_aperture_ap60_dropout0p0_seed42/run/summary.json deleted file mode 100644 index 955bb37..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/02_aperture_ap60_dropout0p0_seed42/run/summary.json +++ /dev/null @@ -1,2622 +0,0 @@ -{ - "reconstruction_bandwidth_hz": 500000, - "reconstruction_source": { - "mode": "simulation" - }, - "clusters": [ - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0060327725038961015, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -320.691671411612, - -481.03750711741804, - -641.383342823224 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.046321440287638786 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.005272737898667883, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -320.2235738069466, - -480.33536071041993, - -640.4471476138932 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04626569514702229 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0047312694272838695, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -317.8032279388882, - -476.70484190833236, - -635.6064558777764 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04572226666640846 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.004200866487451935, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -315.3467548948123, - -473.0201323422185, - -630.6935097896246 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04516619536230458 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0036565083902456493, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -312.9314759402867, - -469.39721391043014, - -625.8629518805734 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04461734980426383 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0031861889638953003, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -310.20409603501, - -465.3061440525151, - -620.40819207002 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04398719954353102 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.00256652716942303, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -308.0188633679521, - -462.0282950519281, - -616.0377267359042 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04348928418531734 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0017486279796490286, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -308.1907975338834, - -462.2861963008251, - -616.3815950677669 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04355435089147969 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.001125078113947233, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -309.8379271124607, - -464.75689066869114, - -619.6758542249214 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.043959799624315364 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0006028161597729909, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -312.11941106984887, - -468.17911660477336, - -624.2388221396977 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04451058220522799 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -2.401284689886321e-5, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -314.2460812377018, - -471.3691218565527, - -628.4921624754036 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.045020721921306135 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0004173119265126402, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -316.95746044523713, - -475.4361906678557, - -633.9149208904743 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04566686911546983 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0011125489663053104, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -318.6137903276162, - -477.9206854914243, - -637.2275806552324 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04605530266618959 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0017115638060362216, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -321.1407190707501, - -481.7110786061252, - -642.2814381415002 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04664759181635218 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0024823400105268, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -319.9942647843374, - -479.99139717650615, - -639.9885295686748 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.0463526618538167 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0031517892589508424, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -318.5017269206361, - -477.7525903809542, - -637.0034538412722 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04597133599677839 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.003865963263819151, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -316.9332073482421, - -475.3998110223631, - -633.8664146964842 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04556339958483016 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0043121080495665395, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -314.38452223900595, - -471.576783358509, - -628.7690444780119 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04492980070620905 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.004534541578681507, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -310.9262770322773, - -466.389415548416, - -621.8525540645546 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.044089545625208976 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.005237053357696269, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -309.35567932143914, - -464.03351898215874, - -618.7113586428783 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04366731027665283 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.005975114360697463, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -308.32565851717015, - -462.4884877757553, - -616.6513170343403 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.0433644115625137 - } - ], - "source_phase_mode": "random_phase_per_window", - "reconstruction_axial_step_m": 5.0e-5, - "detection_truth_mode": "centerline_tube", - "medium": { - "outer_row": 151, - "receiver_row": 1, - "hu_bone_thr": 200, - "slice_index": 250, - "inner_row": 186, - "aberrator": "skull", - "skull_to_transducer": 0.03, - "ct_path": "/Users/vm/INI_code/Ultrasound/DIRU_20240404_human_skull_phase_correction_1_2_(skull_Normal)/DICOM/PAT_0000/STD_0000/SER_0002/OBJ_0001" - }, - "analysis_mode": "detection", - "detection_threshold_ratio": 0.2, - "clean_threshold_ratio": 0.01, - "physical_source_count": 21, - "reconstruction_mode": "windowed", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/02_aperture_ap60_dropout0p0_seed42/run", - "window_info": { - "geometric": { - "effective_window_duration_s": 2.0e-5, - "used_window_ranges": [ - [ - 501, - 1500 - ], - [ - 1001, - 2000 - ], - [ - 1501, - 2500 - ], - [ - 2001, - 3000 - ], - [ - 2501, - 3500 - ], - [ - 3001, - 4000 - ], - [ - 3501, - 4500 - ], - [ - 4001, - 5000 - ], - [ - 4501, - 5500 - ], - [ - 5001, - 6000 - ], - [ - 5501, - 6500 - ], - [ - 6001, - 7000 - ], - [ - 6501, - 7500 - ], - [ - 7001, - 8000 - ], - [ - 7501, - 8500 - ], - [ - 8001, - 9000 - ], - [ - 8501, - 9500 - ], - [ - 9001, - 10000 - ], - [ - 9501, - 10500 - ], - [ - 10001, - 11000 - ], - [ - 10501, - 11500 - ], - [ - 11001, - 12000 - ], - [ - 11501, - 12500 - ], - [ - 12001, - 13000 - ], - [ - 12501, - 13500 - ], - [ - 13001, - 14000 - ], - [ - 13501, - 14500 - ], - [ - 14001, - 15000 - ], - [ - 14501, - 15500 - ], - [ - 15001, - 16000 - ], - [ - 15501, - 16500 - ], - [ - 16001, - 17000 - ], - [ - 16501, - 17500 - ], - [ - 17001, - 18000 - ], - [ - 17501, - 18500 - ], - [ - 18001, - 19000 - ], - [ - 18501, - 19500 - ], - [ - 19001, - 20000 - ], - [ - 19501, - 20500 - ], - [ - 20001, - 21000 - ], - [ - 20501, - 21500 - ], - [ - 21001, - 22000 - ], - [ - 21501, - 22500 - ], - [ - 22001, - 23000 - ], - [ - 22501, - 23500 - ], - [ - 23001, - 24000 - ], - [ - 23501, - 24500 - ], - [ - 24001, - 25000 - ] - ], - "used_window_count": 48, - "skipped_window_ranges": [ - [ - 1, - 1000 - ] - ], - "window_samples": 1000, - "effective_hop_s": 1.0e-5, - "energy_threshold": 450.7279751827604, - "skipped_window_count": 1, - "accumulation": "intensity", - "total_window_count": 49, - "hop_samples": 500 - }, - "hasa": { - "effective_window_duration_s": 2.0e-5, - "used_window_ranges": [ - [ - 501, - 1500 - ], - [ - 1001, - 2000 - ], - [ - 1501, - 2500 - ], - [ - 2001, - 3000 - ], - [ - 2501, - 3500 - ], - [ - 3001, - 4000 - ], - [ - 3501, - 4500 - ], - [ - 4001, - 5000 - ], - [ - 4501, - 5500 - ], - [ - 5001, - 6000 - ], - [ - 5501, - 6500 - ], - [ - 6001, - 7000 - ], - [ - 6501, - 7500 - ], - [ - 7001, - 8000 - ], - [ - 7501, - 8500 - ], - [ - 8001, - 9000 - ], - [ - 8501, - 9500 - ], - [ - 9001, - 10000 - ], - [ - 9501, - 10500 - ], - [ - 10001, - 11000 - ], - [ - 10501, - 11500 - ], - [ - 11001, - 12000 - ], - [ - 11501, - 12500 - ], - [ - 12001, - 13000 - ], - [ - 12501, - 13500 - ], - [ - 13001, - 14000 - ], - [ - 13501, - 14500 - ], - [ - 14001, - 15000 - ], - [ - 14501, - 15500 - ], - [ - 15001, - 16000 - ], - [ - 15501, - 16500 - ], - [ - 16001, - 17000 - ], - [ - 16501, - 17500 - ], - [ - 17001, - 18000 - ], - [ - 17501, - 18500 - ], - [ - 18001, - 19000 - ], - [ - 18501, - 19500 - ], - [ - 19001, - 20000 - ], - [ - 19501, - 20500 - ], - [ - 20001, - 21000 - ], - [ - 20501, - 21500 - ], - [ - 21001, - 22000 - ], - [ - 21501, - 22500 - ], - [ - 22001, - 23000 - ], - [ - 22501, - 23500 - ], - [ - 23001, - 24000 - ], - [ - 23501, - 24500 - ], - [ - 24001, - 25000 - ] - ], - "used_window_count": 48, - "skipped_window_ranges": [ - [ - 1, - 1000 - ] - ], - "window_samples": 1000, - "effective_hop_s": 1.0e-5, - "energy_threshold": 450.7279751827604, - "skipped_window_count": 1, - "accumulation": "intensity", - "total_window_count": 49, - "hop_samples": 500 - } - }, - "window_config": { - "window_duration_s": 1.9999999999999998e-5, - "enabled": true, - "hop_s": 9.999999999999999e-6, - "min_energy_ratio": 0.001, - "taper": "hann", - "accumulation": "intensity" - }, - "activity_boundary_figure": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/02_aperture_ap60_dropout0p0_seed42/run/activity_boundaries.png", - "reference_sound_speed_m_per_s": 1551.0647917287392, - "emission_event_count": 21, - "hasa": { - "truth_components": 1, - "true_negative_pixels": 181631, - "energy_outside_predicted_mask": 1.5097705461381855e10, - "centroid_depth_mm": 45.5138247031815, - "energy_total": 3.689858333298416e10, - "false_negative_area_mm2": 1.32, - "centroid_error_mm": 1.1328610292087822, - "target_axial_spread_mm": 1.3028997817899959, - "prediction_components": 2, - "false_negative_pixels": 33, - "psf_target_ssim_like": 0.26279834019633364, - "matched_prediction_components": 1, - "recall": 0.9614035087719298, - "f1": 0.06852569713642616, - "energy_fraction_outside_predicted_mask": 0.4091676183103161, - "psf_target_mode": "provided_mask", - "energy_outside_mask": 3.555329057293719e10, - "energy_fraction_inside_predicted_mask": 0.5908323816896839, - "axial_spread_mm": 22.074465599137344, - "false_positive_area_mm2": 892.5600000000001, - "missed_truth_components": 0, - "psf_target_normalized_l2_error": 0.9666656097357209, - "psf_target_correlation": 0.3025676051477649, - "true_positive_pixels": 822, - "target_centroid_depth_mm": 44.99999999999995, - "energy_fraction_outside_mask": 0.9635408018810196, - "num_truth_sources": 21, - "recovered_truth_components": 1, - "target_centroid_lateral_mm": 2.0204027711162598e-17, - "psf_axial_fwhm_mm": 0.75, - "energy_inside_mask": 1.3452927600469768e9, - "precision": 0.035529045643153526, - "dice": 0.06852569713642616, - "jaccard": 0.03547844102032889, - "centroid_lateral_mm": -1.0096327480328553, - "peak_mm": [ - 47.199999999999996, - -4.6 - ], - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "predicted_area_mm2": 925.44, - "overlap_area_mm2": 32.88, - "max_intensity": 2.518286377440019e6, - "threshold_ratio": 0.2, - "false_positive_pixels": 22314, - "threshold_db": -6.9897000433601875, - "truth_mask_mode": "provided", - "energy_inside_predicted_mask": 2.1800877871602306e10, - "truth_radius_mm": 1, - "energy_fraction_inside_mask": 0.03645919811898038, - "psf_target_integral": 854.9999999999999, - "truth_area_mm2": 34.2, - "spurious_prediction_components": 1, - "target_lateral_spread_mm": 3.8117734004529122, - "psf_lateral_fwhm_mm": 0.4, - "lateral_spread_mm": 10.086150890440923, - "peak_intensity": 2.518286377440019e6 - }, - "config": { - "axial_dim": 0.08, - "dt": 2.0e-8, - "bottom_margin": 0.01, - "rho0": 1000, - "success_tolerance": 0.0015, - "dx": 0.0002, - "receiver_aperture": 0.06, - "dz": 0.0002, - "zero_pad_factor": 4, - "transverse_dim": 0.1024, - "t_max": 0.0005, - "c0": 1500, - "peak_suppression_radius": 0.008 - }, - "peak_method": "argmax", - "clean_loop_gain": 0.1, - "geometric": { - "truth_components": 1, - "true_negative_pixels": 164533, - "energy_outside_predicted_mask": 6.616710460863403e9, - "centroid_depth_mm": 39.98650268750931, - "energy_total": 2.4727384632452778e10, - "false_negative_area_mm2": 0, - "centroid_error_mm": 5.014320114140363, - "target_axial_spread_mm": 1.3028997817899959, - "prediction_components": 1, - "false_negative_pixels": 0, - "psf_target_ssim_like": 0.24618733266338427, - "matched_prediction_components": 1, - "recall": 1, - "f1": 0.04158358056514761, - "energy_fraction_outside_predicted_mask": 0.2675863444199223, - "psf_target_mode": "provided_mask", - "energy_outside_mask": 2.405450832901562e10, - "energy_fraction_inside_predicted_mask": 0.7324136555800776, - "axial_spread_mm": 23.033967753765992, - "false_positive_area_mm2": 1576.48, - "missed_truth_components": 0, - "psf_target_normalized_l2_error": 0.9761862206843268, - "psf_target_correlation": 0.24329686477373658, - "true_positive_pixels": 855, - "target_centroid_depth_mm": 44.99999999999995, - "energy_fraction_outside_mask": 0.9727882138188582, - "num_truth_sources": 21, - "recovered_truth_components": 1, - "target_centroid_lateral_mm": 2.0204027711162598e-17, - "psf_axial_fwhm_mm": 0.75, - "energy_inside_mask": 6.728763034371569e8, - "precision": 0.021233267936523706, - "dice": 0.04158358056514761, - "jaccard": 0.021233267936523706, - "centroid_lateral_mm": -0.0908344908156004, - "peak_mm": [ - 39, - -0.6000000000000001 - ], - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "predicted_area_mm2": 1610.68, - "overlap_area_mm2": 34.2, - "max_intensity": 1.2957805470203205e6, - "threshold_ratio": 0.2, - "false_positive_pixels": 39412, - "threshold_db": -6.9897000433601875, - "truth_mask_mode": "provided", - "energy_inside_predicted_mask": 1.8110674171589375e10, - "truth_radius_mm": 1, - "energy_fraction_inside_mask": 0.02721178618114181, - "psf_target_integral": 854.9999999999999, - "truth_area_mm2": 34.2, - "spurious_prediction_components": 0, - "target_lateral_spread_mm": 3.8117734004529122, - "psf_lateral_fwhm_mm": 0.4, - "lateral_spread_mm": 10.50032818563355, - "peak_intensity": 1.2957805470203205e6 - }, - "clean_max_iter": 500, - "source_variability": { - "amplitude_distribution": "fixed", - "frequency_jitter_percent": 1, - "amplitude_sigma": 0, - "dropout_probability": 0 - }, - "emission_meta": { - "emission_event_count": 21, - "random_seed": 42, - "harmonics": [ - 2, - 3, - 4 - ], - "phase_jitter_rad": 0.2, - "n_bubbles_per_cluster": [ - 10 - ], - "cluster_model": "vascular", - "phase_mode": "geometric", - "n_emission_sources": 21, - "cavitation_model": "harmonic_cos", - "gate_duration_s": 4.9999999999999996e-5, - "delays_s": [ - 0 - ], - "activity_model": { - "emission_event_count": 21, - "activity_mode": "static", - "phase_jitter_rad": 0, - "active_probability": 1, - "physical_source_count": 21, - "hop_s": null, - "amplitude_jitter": 0, - "frame_duration_s": null - }, - "transducer_m": [ - -0.03, - 0.0 - ], - "fundamental_hz": 500000, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "vascular": { - "branch_levels": 2, - "source_spacing_m": 0.0008, - "bundle_count": 3, - "centerlines_m": [ - [ - [ - 0.0465, - -0.006 - ], - [ - 0.046426584774442725, - -0.0056 - ], - [ - 0.04621352549156242, - -0.0052 - ], - [ - 0.045881677878438706, - -0.0048 - ], - [ - 0.04546352549156242, - -0.0044 - ], - [ - 0.045, - -0.004 - ], - [ - 0.044536474508437576, - -0.0036 - ], - [ - 0.04411832212156129, - -0.0032 - ], - [ - 0.043786474508437576, - -0.0028 - ], - [ - 0.043573415225557265, - -0.0024 - ], - [ - 0.0435, - -0.002 - ], - [ - 0.04357341522555727, - -0.0016 - ], - [ - 0.043786474508437576, - -0.0012 - ], - [ - 0.04411832212156129, - -0.0008 - ], - [ - 0.044536474508437576, - -0.0004 - ], - [ - 0.045, - 0.0 - ], - [ - 0.04546352549156242, - 0.0004 - ], - [ - 0.045881677878438706, - 0.0008 - ], - [ - 0.04621352549156242, - 0.0012 - ], - [ - 0.046426584774442725, - 0.0016 - ], - [ - 0.0465, - 0.002 - ], - [ - 0.04642658477444273, - 0.0024 - ], - [ - 0.04621352549156242, - 0.0028 - ], - [ - 0.045881677878438706, - 0.0032 - ], - [ - 0.04546352549156242, - 0.0036 - ], - [ - 0.045, - 0.004 - ], - [ - 0.044536474508437576, - 0.0044 - ], - [ - 0.04411832212156129, - 0.0048 - ], - [ - 0.043786474508437576, - 0.0052 - ], - [ - 0.04357341522555727, - 0.0056 - ], - [ - 0.0435, - 0.006 - ] - ] - ], - "min_separation_m": 0.0003, - "branch_angle_rad": 0.5235987755982988, - "position_jitter_m": 0.00015, - "squiggle_amplitude_m": 0.0015, - "anchors": [ - { - "segments_m": [ - ], - "anchor_m": [ - 0.045, - 0.0 - ], - "source_count": 21, - "centerlines_m": [ - [ - [ - 0.0465, - -0.006 - ], - [ - 0.046426584774442725, - -0.0056 - ], - [ - 0.04621352549156242, - -0.0052 - ], - [ - 0.045881677878438706, - -0.0048 - ], - [ - 0.04546352549156242, - -0.0044 - ], - [ - 0.045, - -0.004 - ], - [ - 0.044536474508437576, - -0.0036 - ], - [ - 0.04411832212156129, - -0.0032 - ], - [ - 0.043786474508437576, - -0.0028 - ], - [ - 0.043573415225557265, - -0.0024 - ], - [ - 0.0435, - -0.002 - ], - [ - 0.04357341522555727, - -0.0016 - ], - [ - 0.043786474508437576, - -0.0012 - ], - [ - 0.04411832212156129, - -0.0008 - ], - [ - 0.044536474508437576, - -0.0004 - ], - [ - 0.045, - 0.0 - ], - [ - 0.04546352549156242, - 0.0004 - ], - [ - 0.045881677878438706, - 0.0008 - ], - [ - 0.04621352549156242, - 0.0012 - ], - [ - 0.046426584774442725, - 0.0016 - ], - [ - 0.0465, - 0.002 - ], - [ - 0.04642658477444273, - 0.0024 - ], - [ - 0.04621352549156242, - 0.0028 - ], - [ - 0.045881677878438706, - 0.0032 - ], - [ - 0.04546352549156242, - 0.0036 - ], - [ - 0.045, - 0.004 - ], - [ - 0.044536474508437576, - 0.0044 - ], - [ - 0.04411832212156129, - 0.0048 - ], - [ - 0.043786474508437576, - 0.0052 - ], - [ - 0.04357341522555727, - 0.0056 - ], - [ - 0.0435, - 0.006 - ] - ] - ] - } - ], - "branch_scale": 0.65, - "topology": "squiggle", - "max_sources_per_anchor": 0, - "truth_radius_m": 0.001, - "squiggle_wavelength_m": 0.008, - "length_m": 0.012, - "squiggle_slope": 0, - "bundle_spacing_m": 0.002 - }, - "n_anchor_clusters": 1, - "physical_source_count": 21, - "anchor_clusters_m": [ - [ - 0.045, - 0.0 - ] - ] - }, - "n_realizations": 1, - "activity_model": { - "emission_event_count": 21, - "activity_mode": "static", - "phase_jitter_rad": 0, - "active_probability": 1, - "physical_source_count": 21, - "hop_s": null, - "amplitude_jitter": 0, - "frame_duration_s": null - }, - "detection_truth_radius_m": 0.001, - "boundary_threshold_ratios": [ - 0.6, - 0.65, - 0.7 - ], - "simulation": { - "receiver_row": 1, - "source_indices": [ - [ - 233, - 227 - ], - [ - 232, - 231 - ], - [ - 230, - 233 - ], - [ - 227, - 236 - ], - [ - 224, - 239 - ], - [ - 221, - 241 - ], - [ - 218, - 244 - ], - [ - 219, - 248 - ], - [ - 221, - 251 - ], - [ - 224, - 254 - ], - [ - 226, - 257 - ], - [ - 229, - 259 - ], - [ - 231, - 263 - ], - [ - 234, - 266 - ], - [ - 233, - 269 - ], - [ - 231, - 273 - ], - [ - 229, - 276 - ], - [ - 226, - 279 - ], - [ - 221, - 280 - ], - [ - 219, - 283 - ], - [ - 218, - 287 - ] - ], - "receiver_cols": [ - 107, - 406 - ] - }, - "reconstruction_frequencies_hz": [ - 1000000, - 1500000, - 2000000 - ], - "activity_boundary_metrics": { - "geometric": [ - { - "target_centroid_lateral_mm": 2.0204027711162598e-17, - "axial_spread_mm": 23.033967753765992, - "peak_intensity": 1.2957805470203205e6, - "dice": 0.24530436750396017, - "false_positive_area_mm2": 120.88, - "energy_inside_predicted_mask": 3.183285886450722e9, - "overlap_area_mm2": 21.68, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.8712647563109887, - "centroid_lateral_mm": -0.0908344908156004, - "f1": 0.24530436750396017, - "threshold_ratio": 0.6, - "false_positive_pixels": 3022, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.4, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.99999999999995, - "true_negative_pixels": 200923, - "energy_outside_predicted_mask": 2.1544098746002056e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 2.4727384632452778e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 3, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899959, - "jaccard": 0.13979881351560486, - "psf_target_correlation": 0.24329686477373658, - "psf_target_ssim_like": 0.24618733266338427, - "precision": 0.1520763187429854, - "peak_mm": [ - 39, - -0.6000000000000001 - ], - "energy_fraction_inside_predicted_mask": 0.12873524368901132, - "recovered_truth_components": 1, - "lateral_spread_mm": 10.50032818563355, - "true_positive_pixels": 542, - "centroid_error_mm": 5.014320114140363, - "recall": 0.6339181286549708, - "matched_prediction_components": 2, - "prediction_components": 5, - "false_negative_pixels": 313, - "energy_fraction_inside_mask": 0.02721178618114181, - "predicted_area_mm2": 142.56, - "energy_fraction_outside_mask": 0.9727882138188582, - "max_intensity": 1.2957805470203205e6, - "energy_inside_mask": 6.728763034371569e8, - "centroid_depth_mm": 39.98650268750931, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8117734004529122, - "psf_target_normalized_l2_error": 0.9761862206843268, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 12.52, - "energy_outside_mask": 2.405450832901562e10, - "num_truth_sources": 21, - "threshold_db": -2.218487496163564 - }, - { - "target_centroid_lateral_mm": 2.0204027711162598e-17, - "axial_spread_mm": 23.033967753765992, - "peak_intensity": 1.2957805470203205e6, - "dice": 0.2620552045227802, - "false_positive_area_mm2": 70.32000000000001, - "energy_inside_predicted_mask": 2.0442157098003864e9, - "overlap_area_mm2": 15.76, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.917329885865992, - "centroid_lateral_mm": -0.0908344908156004, - "f1": 0.2620552045227802, - "threshold_ratio": 0.65, - "false_positive_pixels": 1758, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.4, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.99999999999995, - "true_negative_pixels": 202187, - "energy_outside_predicted_mask": 2.268316892265239e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 2.4727384632452778e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 4, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899959, - "jaccard": 0.15078453884424034, - "psf_target_correlation": 0.24329686477373658, - "psf_target_ssim_like": 0.24618733266338427, - "precision": 0.18308550185873607, - "peak_mm": [ - 39, - -0.6000000000000001 - ], - "energy_fraction_inside_predicted_mask": 0.08267011413400799, - "recovered_truth_components": 1, - "lateral_spread_mm": 10.50032818563355, - "true_positive_pixels": 394, - "centroid_error_mm": 5.014320114140363, - "recall": 0.4608187134502924, - "matched_prediction_components": 3, - "prediction_components": 7, - "false_negative_pixels": 461, - "energy_fraction_inside_mask": 0.02721178618114181, - "predicted_area_mm2": 86.08, - "energy_fraction_outside_mask": 0.9727882138188582, - "max_intensity": 1.2957805470203205e6, - "energy_inside_mask": 6.728763034371569e8, - "centroid_depth_mm": 39.98650268750931, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8117734004529122, - "psf_target_normalized_l2_error": 0.9761862206843268, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 18.44, - "energy_outside_mask": 2.405450832901562e10, - "num_truth_sources": 21, - "threshold_db": -1.8708664335714442 - }, - { - "target_centroid_lateral_mm": 2.0204027711162598e-17, - "axial_spread_mm": 23.033967753765992, - "peak_intensity": 1.2957805470203205e6, - "dice": 0.21961520412951666, - "false_positive_area_mm2": 41.68, - "energy_inside_predicted_mask": 1.2808781860675778e9, - "overlap_area_mm2": 9.36, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.9482000136647478, - "centroid_lateral_mm": -0.0908344908156004, - "f1": 0.21961520412951668, - "threshold_ratio": 0.7, - "false_positive_pixels": 1042, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.4, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.99999999999995, - "true_negative_pixels": 202903, - "energy_outside_predicted_mask": 2.34465064463852e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 2.4727384632452778e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 3, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899959, - "jaccard": 0.12335266209804956, - "psf_target_correlation": 0.24329686477373658, - "psf_target_ssim_like": 0.24618733266338427, - "precision": 0.1833855799373041, - "peak_mm": [ - 39, - -0.6000000000000001 - ], - "energy_fraction_inside_predicted_mask": 0.051799986335252145, - "recovered_truth_components": 1, - "lateral_spread_mm": 10.50032818563355, - "true_positive_pixels": 234, - "centroid_error_mm": 5.014320114140363, - "recall": 0.2736842105263158, - "matched_prediction_components": 2, - "prediction_components": 5, - "false_negative_pixels": 621, - "energy_fraction_inside_mask": 0.02721178618114181, - "predicted_area_mm2": 51.04, - "energy_fraction_outside_mask": 0.9727882138188582, - "max_intensity": 1.2957805470203205e6, - "energy_inside_mask": 6.728763034371569e8, - "centroid_depth_mm": 39.98650268750931, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8117734004529122, - "psf_target_normalized_l2_error": 0.9761862206843268, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 24.84, - "energy_outside_mask": 2.405450832901562e10, - "num_truth_sources": 21, - "threshold_db": -1.5490195998574319 - } - ], - "hasa": [ - { - "target_centroid_lateral_mm": 2.0204027711162598e-17, - "axial_spread_mm": 22.074465599137344, - "peak_intensity": 2.518286377440019e6, - "dice": 0.317016317016317, - "false_positive_area_mm2": 66.88, - "energy_inside_predicted_mask": 3.778130002977122e9, - "overlap_area_mm2": 19.04, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.8976077219853641, - "centroid_lateral_mm": -1.0096327480328553, - "f1": 0.317016317016317, - "threshold_ratio": 0.6, - "false_positive_pixels": 1672, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.4, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.99999999999995, - "true_negative_pixels": 202273, - "energy_outside_predicted_mask": 3.312045333000704e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 3.689858333298416e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 4, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899959, - "jaccard": 0.1883656509695291, - "psf_target_correlation": 0.3025676051477649, - "psf_target_ssim_like": 0.26279834019633364, - "precision": 0.22160148975791433, - "peak_mm": [ - 47.199999999999996, - -4.6 - ], - "energy_fraction_inside_predicted_mask": 0.10239227801463582, - "recovered_truth_components": 1, - "lateral_spread_mm": 10.086150890440923, - "true_positive_pixels": 476, - "centroid_error_mm": 1.1328610292087822, - "recall": 0.5567251461988304, - "matched_prediction_components": 2, - "prediction_components": 6, - "false_negative_pixels": 379, - "energy_fraction_inside_mask": 0.03645919811898038, - "predicted_area_mm2": 85.92, - "energy_fraction_outside_mask": 0.9635408018810196, - "max_intensity": 2.518286377440019e6, - "energy_inside_mask": 1.3452927600469768e9, - "centroid_depth_mm": 45.5138247031815, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8117734004529122, - "psf_target_normalized_l2_error": 0.9666656097357209, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 15.16, - "energy_outside_mask": 3.555329057293719e10, - "num_truth_sources": 21, - "threshold_db": -2.218487496163564 - }, - { - "target_centroid_lateral_mm": 2.0204027711162598e-17, - "axial_spread_mm": 22.074465599137344, - "peak_intensity": 2.518286377440019e6, - "dice": 0.3654871546291808, - "false_positive_area_mm2": 33.24, - "energy_inside_predicted_mask": 2.3089502341117043e9, - "overlap_area_mm2": 15.08, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.937424420518397, - "centroid_lateral_mm": -1.0096327480328553, - "f1": 0.36548715462918085, - "threshold_ratio": 0.65, - "false_positive_pixels": 831, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.4, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.99999999999995, - "true_negative_pixels": 203114, - "energy_outside_predicted_mask": 3.458963309887246e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 3.689858333298416e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 1, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899959, - "jaccard": 0.2236061684460261, - "psf_target_correlation": 0.3025676051477649, - "psf_target_ssim_like": 0.26279834019633364, - "precision": 0.3120860927152318, - "peak_mm": [ - 47.199999999999996, - -4.6 - ], - "energy_fraction_inside_predicted_mask": 0.06257557948160306, - "recovered_truth_components": 1, - "lateral_spread_mm": 10.086150890440923, - "true_positive_pixels": 377, - "centroid_error_mm": 1.1328610292087822, - "recall": 0.4409356725146199, - "matched_prediction_components": 3, - "prediction_components": 4, - "false_negative_pixels": 478, - "energy_fraction_inside_mask": 0.03645919811898038, - "predicted_area_mm2": 48.32, - "energy_fraction_outside_mask": 0.9635408018810196, - "max_intensity": 2.518286377440019e6, - "energy_inside_mask": 1.3452927600469768e9, - "centroid_depth_mm": 45.5138247031815, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8117734004529122, - "psf_target_normalized_l2_error": 0.9666656097357209, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 19.12, - "energy_outside_mask": 3.555329057293719e10, - "num_truth_sources": 21, - "threshold_db": -1.8708664335714442 - }, - { - "target_centroid_lateral_mm": 2.0204027711162598e-17, - "axial_spread_mm": 22.074465599137344, - "peak_intensity": 2.518286377440019e6, - "dice": 0.3400236127508855, - "false_positive_area_mm2": 22.04, - "energy_inside_predicted_mask": 1.6853699489130065e9, - "overlap_area_mm2": 11.52, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.9543242640590368, - "centroid_lateral_mm": -1.0096327480328553, - "f1": 0.3400236127508855, - "threshold_ratio": 0.7, - "false_positive_pixels": 551, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.4, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.99999999999995, - "true_negative_pixels": 203394, - "energy_outside_predicted_mask": 3.521321338407115e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 3.689858333298416e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 0, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899959, - "jaccard": 0.20483641536273114, - "psf_target_correlation": 0.3025676051477649, - "psf_target_ssim_like": 0.26279834019633364, - "precision": 0.3432657926102503, - "peak_mm": [ - 47.199999999999996, - -4.6 - ], - "energy_fraction_inside_predicted_mask": 0.045675735940963094, - "recovered_truth_components": 1, - "lateral_spread_mm": 10.086150890440923, - "true_positive_pixels": 288, - "centroid_error_mm": 1.1328610292087822, - "recall": 0.3368421052631579, - "matched_prediction_components": 2, - "prediction_components": 2, - "false_negative_pixels": 567, - "energy_fraction_inside_mask": 0.03645919811898038, - "predicted_area_mm2": 33.56, - "energy_fraction_outside_mask": 0.9635408018810196, - "max_intensity": 2.518286377440019e6, - "energy_inside_mask": 1.3452927600469768e9, - "centroid_depth_mm": 45.5138247031815, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8117734004529122, - "psf_target_normalized_l2_error": 0.9666656097357209, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 22.68, - "energy_outside_mask": 3.555329057293719e10, - "num_truth_sources": 21, - "threshold_db": -1.5490195998574319 - } - ], - "threshold_ratios": [ - 0.6, - 0.65, - 0.7 - ] - } -} \ No newline at end of file diff --git a/runs/20260506_103348_pam_aperture_sweep/02_aperture_ap60_dropout0p0_seed42/status.json b/runs/20260506_103348_pam_aperture_sweep/02_aperture_ap60_dropout0p0_seed42/status.json deleted file mode 100644 index 683cf08..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/02_aperture_ap60_dropout0p0_seed42/status.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "best_hasa_f1": 0.36548715462918085, - "best_hasa_recall": 0.4409356725146199, - "hasa_psf_corr": 0.3025676051477649, - "best_geo_threshold": 0.65, - "hasa_centroid_error_mm": 1.1328610292087822, - "best_hasa_jaccard": 0.2236061684460261, - "summary_found": true, - "kind": "simulation", - "best_hasa_precision": 0.3120860927152318, - "status": "success", - "elapsed_min": 6.42, - "id": "aperture_ap60_dropout0p0_seed42", - "best_geo_f1": 0.2620552045227802, - "cmd": "`/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/02_aperture_ap60_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=60 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle`", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/02_aperture_ap60_dropout0p0_seed42/run", - "hasa_psf_l2": 0.9666656097357209, - "best_hasa_threshold": 0.65 -} diff --git a/runs/20260506_103348_pam_aperture_sweep/03_aperture_ap100_dropout0p0_seed42/run.log b/runs/20260506_103348_pam_aperture_sweep/03_aperture_ap100_dropout0p0_seed42/run.log deleted file mode 100644 index 8cc39b8..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/03_aperture_ap100_dropout0p0_seed42/run.log +++ /dev/null @@ -1,81 +0,0 @@ -# 2026-05-06T10:46:39.540 -# `/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/03_aperture_ap100_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=100 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle` -WARNING:root:Highest prime factors in each dimension are [11 23] -WARNING:root:Use dimension sizes with lower prime factors to improve speed -┌───────────────────────────────────────────────────────────────┐ -│ kspaceFirstOrder-OMP v1.3 │ -├───────────────────────────────────────────────────────────────┤ -│ Reading simulation configuration: Done │ -│ Number of CPU threads: 12 │ -│ Processor name: │ -├───────────────────────────────────────────────────────────────┤ -│ Simulation details │ -├───────────────────────────────────────────────────────────────┤ -│ Domain dimensions: 440 x 552 │ -│ Medium type: 2D │ -│ Simulation time steps: 25000 │ -├───────────────────────────────────────────────────────────────┤ -│ Initialization │ -├───────────────────────────────────────────────────────────────┤ -│ Memory allocation: Done │ -│ Data loading: Done │ -│ Elapsed time: 0.00s │ -├───────────────────────────────────────────────────────────────┤ -│ FFT plans creation: Done │ -│ Pre-processing phase: Done │ -│ Elapsed time: 0.02s │ -├───────────────────────────────────────────────────────────────┤ -│ Computational resources │ -├───────────────────────────────────────────────────────────────┤ -│ Current host memory in use: 47261471662MB │ -│ Expected output file size: 47MB │ -├───────────────────────────────────────────────────────────────┤ -│ Simulation │ -├──────────┬────────────────┬──────────────┬────────────────────┤ -│ Progress │ Elapsed time │ Time to go │ Est. finish time │ -├──────────┼────────────────┼──────────────┼────────────────────┤ -│ 0% │ 0.009s │ 118.467s │ 06/05/26 10:49:12 │ -│ 5% │ 4.928s │ 93.468s │ 06/05/26 10:48:52 │ -│ 10% │ 9.820s │ 88.303s │ 06/05/26 10:48:52 │ -│ 15% │ 14.696s │ 83.222s │ 06/05/26 10:48:52 │ -│ 20% │ 19.572s │ 78.249s │ 06/05/26 10:48:52 │ -│ 25% │ 24.458s │ 73.343s │ 06/05/26 10:48:52 │ -│ 30% │ 29.366s │ 68.494s │ 06/05/26 10:48:52 │ -│ 35% │ 34.243s │ 63.571s │ 06/05/26 10:48:52 │ -│ 40% │ 39.123s │ 58.665s │ 06/05/26 10:48:52 │ -│ 45% │ 44.004s │ 53.765s │ 06/05/26 10:48:51 │ -│ 50% │ 48.860s │ 48.844s │ 06/05/26 10:48:51 │ -│ 55% │ 53.746s │ 43.960s │ 06/05/26 10:48:51 │ -│ 60% │ 58.627s │ 39.072s │ 06/05/26 10:48:52 │ -│ 65% │ 63.494s │ 34.177s │ 06/05/26 10:48:52 │ -│ 70% │ 68.379s │ 29.294s │ 06/05/26 10:48:52 │ -│ 75% │ 73.273s │ 24.414s │ 06/05/26 10:48:52 │ -│ 80% │ 78.149s │ 19.528s │ 06/05/26 10:48:52 │ -│ 85% │ 83.026s │ 14.642s │ 06/05/26 10:48:51 │ -│ 90% │ 87.917s │ 9.760s │ 06/05/26 10:48:51 │ -│ 95% │ 92.806s │ 4.876s │ 06/05/26 10:48:51 │ -├──────────┴────────────────┴──────────────┴────────────────────┤ -│ Elapsed time: 97.68s │ -├───────────────────────────────────────────────────────────────┤ -│ Sampled data post-processing: Done │ -│ Elapsed time: 0.00s │ -├───────────────────────────────────────────────────────────────┤ -│ Summary │ -├───────────────────────────────────────────────────────────────┤ -│ Peak memory in use: 6166913976MB │ -├───────────────────────────────────────────────────────────────┤ -│ Total execution time: 97.73s │ -├───────────────────────────────────────────────────────────────┤ -│ End of computation │ -└───────────────────────────────────────────────────────────────┘ - Activating project at `~/INI_code/Julia II` -┌ Info: reading DICOM series -└ dicom_dir = "/Users/vm/INI_code/Ultrasound/DIRU_20240404_human_skull_phase_correction_1_2_(skull_Normal)/DICOM/PAT_0000/STD_0000/SER_0002/OBJ_0001" -┌ Info: cropping ROI -│ index_xyz = (170, 190, 400) -└ size_xyz = (705, 360, 450) -┌ Info: resampling x/y only -│ spacing_x_mm = 0.201171875 -│ spacing_y_mm = 0.201171875 -└ new_spacing_xy_mm = 0.2 -Saved PAM cluster outputs to /Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/03_aperture_ap100_dropout0p0_seed42/run diff --git a/runs/20260506_103348_pam_aperture_sweep/03_aperture_ap100_dropout0p0_seed42/run/activity_boundaries.png b/runs/20260506_103348_pam_aperture_sweep/03_aperture_ap100_dropout0p0_seed42/run/activity_boundaries.png deleted file mode 100644 index ac25c04..0000000 Binary files a/runs/20260506_103348_pam_aperture_sweep/03_aperture_ap100_dropout0p0_seed42/run/activity_boundaries.png and /dev/null differ diff --git a/runs/20260506_103348_pam_aperture_sweep/03_aperture_ap100_dropout0p0_seed42/run/overview.png b/runs/20260506_103348_pam_aperture_sweep/03_aperture_ap100_dropout0p0_seed42/run/overview.png deleted file mode 100644 index c5985e4..0000000 Binary files a/runs/20260506_103348_pam_aperture_sweep/03_aperture_ap100_dropout0p0_seed42/run/overview.png and /dev/null differ diff --git a/runs/20260506_103348_pam_aperture_sweep/03_aperture_ap100_dropout0p0_seed42/run/summary.json b/runs/20260506_103348_pam_aperture_sweep/03_aperture_ap100_dropout0p0_seed42/run/summary.json deleted file mode 100644 index e5b5456..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/03_aperture_ap100_dropout0p0_seed42/run/summary.json +++ /dev/null @@ -1,2622 +0,0 @@ -{ - "reconstruction_bandwidth_hz": 500000, - "reconstruction_source": { - "mode": "simulation" - }, - "clusters": [ - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0060327725038961015, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -320.691671411612, - -481.03750711741804, - -641.383342823224 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.046321440287638786 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.005272737898667883, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -320.2235738069466, - -480.33536071041993, - -640.4471476138932 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04626569514702229 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0047312694272838695, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -317.8032279388882, - -476.70484190833236, - -635.6064558777764 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04572226666640846 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.004200866487451935, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -315.3467548948123, - -473.0201323422185, - -630.6935097896246 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04516619536230458 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0036565083902456493, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -312.9314759402867, - -469.39721391043014, - -625.8629518805734 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04461734980426383 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0031861889638953003, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -310.20409603501, - -465.3061440525151, - -620.40819207002 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04398719954353102 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.00256652716942303, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -308.0188633679521, - -462.0282950519281, - -616.0377267359042 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04348928418531734 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0017486279796490286, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -308.1907975338834, - -462.2861963008251, - -616.3815950677669 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04355435089147969 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.001125078113947233, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -309.8379271124607, - -464.75689066869114, - -619.6758542249214 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.043959799624315364 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0006028161597729909, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -312.11941106984887, - -468.17911660477336, - -624.2388221396977 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04451058220522799 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -2.401284689886321e-5, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -314.2460812377018, - -471.3691218565527, - -628.4921624754036 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.045020721921306135 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0004173119265126402, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -316.95746044523713, - -475.4361906678557, - -633.9149208904743 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04566686911546983 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0011125489663053104, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -318.6137903276162, - -477.9206854914243, - -637.2275806552324 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04605530266618959 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0017115638060362216, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -321.1407190707501, - -481.7110786061252, - -642.2814381415002 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04664759181635218 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0024823400105268, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -319.9942647843374, - -479.99139717650615, - -639.9885295686748 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.0463526618538167 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0031517892589508424, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -318.5017269206361, - -477.7525903809542, - -637.0034538412722 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04597133599677839 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.003865963263819151, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -316.9332073482421, - -475.3998110223631, - -633.8664146964842 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04556339958483016 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0043121080495665395, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -314.38452223900595, - -471.576783358509, - -628.7690444780119 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04492980070620905 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.004534541578681507, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -310.9262770322773, - -466.389415548416, - -621.8525540645546 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.044089545625208976 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.005237053357696269, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -309.35567932143914, - -464.03351898215874, - -618.7113586428783 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04366731027665283 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.005975114360697463, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -308.32565851717015, - -462.4884877757553, - -616.6513170343403 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.0433644115625137 - } - ], - "source_phase_mode": "random_phase_per_window", - "reconstruction_axial_step_m": 5.0e-5, - "detection_truth_mode": "centerline_tube", - "medium": { - "outer_row": 151, - "receiver_row": 1, - "hu_bone_thr": 200, - "slice_index": 250, - "inner_row": 186, - "aberrator": "skull", - "skull_to_transducer": 0.03, - "ct_path": "/Users/vm/INI_code/Ultrasound/DIRU_20240404_human_skull_phase_correction_1_2_(skull_Normal)/DICOM/PAT_0000/STD_0000/SER_0002/OBJ_0001" - }, - "analysis_mode": "detection", - "detection_threshold_ratio": 0.2, - "clean_threshold_ratio": 0.01, - "physical_source_count": 21, - "reconstruction_mode": "windowed", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/03_aperture_ap100_dropout0p0_seed42/run", - "window_info": { - "geometric": { - "effective_window_duration_s": 2.0e-5, - "used_window_ranges": [ - [ - 1001, - 2000 - ], - [ - 1501, - 2500 - ], - [ - 2001, - 3000 - ], - [ - 2501, - 3500 - ], - [ - 3001, - 4000 - ], - [ - 3501, - 4500 - ], - [ - 4001, - 5000 - ], - [ - 4501, - 5500 - ], - [ - 5001, - 6000 - ], - [ - 5501, - 6500 - ], - [ - 6001, - 7000 - ], - [ - 6501, - 7500 - ], - [ - 7001, - 8000 - ], - [ - 7501, - 8500 - ], - [ - 8001, - 9000 - ], - [ - 8501, - 9500 - ], - [ - 9001, - 10000 - ], - [ - 9501, - 10500 - ], - [ - 10001, - 11000 - ], - [ - 10501, - 11500 - ], - [ - 11001, - 12000 - ], - [ - 11501, - 12500 - ], - [ - 12001, - 13000 - ], - [ - 12501, - 13500 - ], - [ - 13001, - 14000 - ], - [ - 13501, - 14500 - ], - [ - 14001, - 15000 - ], - [ - 14501, - 15500 - ], - [ - 15001, - 16000 - ], - [ - 15501, - 16500 - ], - [ - 16001, - 17000 - ], - [ - 16501, - 17500 - ], - [ - 17001, - 18000 - ], - [ - 17501, - 18500 - ], - [ - 18001, - 19000 - ], - [ - 18501, - 19500 - ], - [ - 19001, - 20000 - ], - [ - 19501, - 20500 - ], - [ - 20001, - 21000 - ], - [ - 20501, - 21500 - ], - [ - 21001, - 22000 - ], - [ - 21501, - 22500 - ], - [ - 22001, - 23000 - ], - [ - 22501, - 23500 - ], - [ - 23001, - 24000 - ], - [ - 23501, - 24500 - ], - [ - 24001, - 25000 - ] - ], - "used_window_count": 47, - "skipped_window_ranges": [ - [ - 1, - 1000 - ], - [ - 501, - 1500 - ] - ], - "window_samples": 1000, - "effective_hop_s": 1.0e-5, - "energy_threshold": 598.6043211649574, - "skipped_window_count": 2, - "accumulation": "intensity", - "total_window_count": 49, - "hop_samples": 500 - }, - "hasa": { - "effective_window_duration_s": 2.0e-5, - "used_window_ranges": [ - [ - 1001, - 2000 - ], - [ - 1501, - 2500 - ], - [ - 2001, - 3000 - ], - [ - 2501, - 3500 - ], - [ - 3001, - 4000 - ], - [ - 3501, - 4500 - ], - [ - 4001, - 5000 - ], - [ - 4501, - 5500 - ], - [ - 5001, - 6000 - ], - [ - 5501, - 6500 - ], - [ - 6001, - 7000 - ], - [ - 6501, - 7500 - ], - [ - 7001, - 8000 - ], - [ - 7501, - 8500 - ], - [ - 8001, - 9000 - ], - [ - 8501, - 9500 - ], - [ - 9001, - 10000 - ], - [ - 9501, - 10500 - ], - [ - 10001, - 11000 - ], - [ - 10501, - 11500 - ], - [ - 11001, - 12000 - ], - [ - 11501, - 12500 - ], - [ - 12001, - 13000 - ], - [ - 12501, - 13500 - ], - [ - 13001, - 14000 - ], - [ - 13501, - 14500 - ], - [ - 14001, - 15000 - ], - [ - 14501, - 15500 - ], - [ - 15001, - 16000 - ], - [ - 15501, - 16500 - ], - [ - 16001, - 17000 - ], - [ - 16501, - 17500 - ], - [ - 17001, - 18000 - ], - [ - 17501, - 18500 - ], - [ - 18001, - 19000 - ], - [ - 18501, - 19500 - ], - [ - 19001, - 20000 - ], - [ - 19501, - 20500 - ], - [ - 20001, - 21000 - ], - [ - 20501, - 21500 - ], - [ - 21001, - 22000 - ], - [ - 21501, - 22500 - ], - [ - 22001, - 23000 - ], - [ - 22501, - 23500 - ], - [ - 23001, - 24000 - ], - [ - 23501, - 24500 - ], - [ - 24001, - 25000 - ] - ], - "used_window_count": 47, - "skipped_window_ranges": [ - [ - 1, - 1000 - ], - [ - 501, - 1500 - ] - ], - "window_samples": 1000, - "effective_hop_s": 1.0e-5, - "energy_threshold": 598.6043211649574, - "skipped_window_count": 2, - "accumulation": "intensity", - "total_window_count": 49, - "hop_samples": 500 - } - }, - "window_config": { - "window_duration_s": 1.9999999999999998e-5, - "enabled": true, - "hop_s": 9.999999999999999e-6, - "min_energy_ratio": 0.001, - "taper": "hann", - "accumulation": "intensity" - }, - "activity_boundary_figure": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/03_aperture_ap100_dropout0p0_seed42/run/activity_boundaries.png", - "reference_sound_speed_m_per_s": 1551.0647917287392, - "emission_event_count": 21, - "hasa": { - "truth_components": 1, - "true_negative_pixels": 183123, - "energy_outside_predicted_mask": 2.4835205440813755e10, - "centroid_depth_mm": 45.28174612201426, - "energy_total": 5.0299810024956375e10, - "false_negative_area_mm2": 0.56, - "centroid_error_mm": 1.2868821478349797, - "target_axial_spread_mm": 1.3028997817899959, - "prediction_components": 3, - "false_negative_pixels": 14, - "psf_target_ssim_like": 0.26140716514094037, - "matched_prediction_components": 1, - "recall": 0.9836257309941521, - "f1": 0.07469579891642242, - "energy_fraction_outside_predicted_mask": 0.4937435236533042, - "psf_target_mode": "provided_mask", - "energy_outside_mask": 4.852190841173286e10, - "energy_fraction_inside_predicted_mask": 0.5062564763466958, - "axial_spread_mm": 22.162172739723236, - "false_positive_area_mm2": 832.88, - "missed_truth_components": 0, - "psf_target_normalized_l2_error": 0.9661709715544975, - "psf_target_correlation": 0.3321059725982636, - "true_positive_pixels": 841, - "target_centroid_depth_mm": 44.99999999999995, - "energy_fraction_outside_mask": 0.9646539099781608, - "num_truth_sources": 21, - "recovered_truth_components": 1, - "target_centroid_lateral_mm": 2.0204027711162598e-17, - "psf_axial_fwhm_mm": 0.75, - "energy_inside_mask": 1.7779016132235167e9, - "precision": 0.038821954484605084, - "dice": 0.07469579891642242, - "jaccard": 0.03879688148729068, - "centroid_lateral_mm": -1.255661094860499, - "peak_mm": [ - 44, - -3 - ], - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "predicted_area_mm2": 866.52, - "overlap_area_mm2": 33.64, - "max_intensity": 3.2973654880032944e6, - "threshold_ratio": 0.2, - "false_positive_pixels": 20822, - "threshold_db": -6.9897000433601875, - "truth_mask_mode": "provided", - "energy_inside_predicted_mask": 2.546460458414262e10, - "truth_radius_mm": 1, - "energy_fraction_inside_mask": 0.03534609002183917, - "psf_target_integral": 854.9999999999999, - "truth_area_mm2": 34.2, - "spurious_prediction_components": 2, - "target_lateral_spread_mm": 3.8117734004529122, - "psf_lateral_fwhm_mm": 0.4, - "lateral_spread_mm": 13.246029736744822, - "peak_intensity": 3.2973654880032944e6 - }, - "config": { - "axial_dim": 0.08, - "dt": 2.0e-8, - "bottom_margin": 0.01, - "rho0": 1000, - "success_tolerance": 0.0015, - "dx": 0.0002, - "receiver_aperture": 0.1, - "dz": 0.0002, - "zero_pad_factor": 4, - "transverse_dim": 0.1024, - "t_max": 0.0005, - "c0": 1500, - "peak_suppression_radius": 0.008 - }, - "peak_method": "argmax", - "clean_loop_gain": 0.1, - "geometric": { - "truth_components": 1, - "true_negative_pixels": 171297, - "energy_outside_predicted_mask": 1.5019792126634026e10, - "centroid_depth_mm": 39.962496140400724, - "energy_total": 3.427356885681243e10, - "false_negative_area_mm2": 0, - "centroid_error_mm": 5.067899499634459, - "target_axial_spread_mm": 1.3028997817899959, - "prediction_components": 3, - "false_negative_pixels": 0, - "psf_target_ssim_like": 0.24635239694466196, - "matched_prediction_components": 1, - "recall": 1, - "f1": 0.049770068106408986, - "energy_fraction_outside_predicted_mask": 0.43823251057931767, - "psf_target_mode": "provided_mask", - "energy_outside_mask": 3.3350594670178715e10, - "energy_fraction_inside_predicted_mask": 0.5617674894206823, - "axial_spread_mm": 23.039685278514263, - "false_positive_area_mm2": 1305.92, - "missed_truth_components": 0, - "psf_target_normalized_l2_error": 0.9746647981267593, - "psf_target_correlation": 0.284469648900298, - "true_positive_pixels": 855, - "target_centroid_depth_mm": 44.99999999999995, - "energy_fraction_outside_mask": 0.9730703799627723, - "num_truth_sources": 21, - "recovered_truth_components": 1, - "target_centroid_lateral_mm": 2.0204027711162598e-17, - "psf_axial_fwhm_mm": 0.75, - "energy_inside_mask": 9.229741866337175e8, - "precision": 0.025520102677372175, - "dice": 0.049770068106408986, - "jaccard": 0.025520102677372175, - "centroid_lateral_mm": -0.5542203559218081, - "peak_mm": [ - 40.4, - -0.8 - ], - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "predicted_area_mm2": 1340.1200000000001, - "overlap_area_mm2": 34.2, - "max_intensity": 1.6894841691673698e6, - "threshold_ratio": 0.2, - "false_positive_pixels": 32648, - "threshold_db": -6.9897000433601875, - "truth_mask_mode": "provided", - "energy_inside_predicted_mask": 1.9253776730178406e10, - "truth_radius_mm": 1, - "energy_fraction_inside_mask": 0.02692962003722765, - "psf_target_integral": 854.9999999999999, - "truth_area_mm2": 34.2, - "spurious_prediction_components": 2, - "target_lateral_spread_mm": 3.8117734004529122, - "psf_lateral_fwhm_mm": 0.4, - "lateral_spread_mm": 14.2823287871535, - "peak_intensity": 1.6894841691673698e6 - }, - "clean_max_iter": 500, - "source_variability": { - "amplitude_distribution": "fixed", - "frequency_jitter_percent": 1, - "amplitude_sigma": 0, - "dropout_probability": 0 - }, - "emission_meta": { - "emission_event_count": 21, - "random_seed": 42, - "harmonics": [ - 2, - 3, - 4 - ], - "phase_jitter_rad": 0.2, - "n_bubbles_per_cluster": [ - 10 - ], - "cluster_model": "vascular", - "phase_mode": "geometric", - "n_emission_sources": 21, - "cavitation_model": "harmonic_cos", - "gate_duration_s": 4.9999999999999996e-5, - "delays_s": [ - 0 - ], - "activity_model": { - "emission_event_count": 21, - "activity_mode": "static", - "phase_jitter_rad": 0, - "active_probability": 1, - "physical_source_count": 21, - "hop_s": null, - "amplitude_jitter": 0, - "frame_duration_s": null - }, - "transducer_m": [ - -0.03, - 0.0 - ], - "fundamental_hz": 500000, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "vascular": { - "branch_levels": 2, - "source_spacing_m": 0.0008, - "bundle_count": 3, - "centerlines_m": [ - [ - [ - 0.0465, - -0.006 - ], - [ - 0.046426584774442725, - -0.0056 - ], - [ - 0.04621352549156242, - -0.0052 - ], - [ - 0.045881677878438706, - -0.0048 - ], - [ - 0.04546352549156242, - -0.0044 - ], - [ - 0.045, - -0.004 - ], - [ - 0.044536474508437576, - -0.0036 - ], - [ - 0.04411832212156129, - -0.0032 - ], - [ - 0.043786474508437576, - -0.0028 - ], - [ - 0.043573415225557265, - -0.0024 - ], - [ - 0.0435, - -0.002 - ], - [ - 0.04357341522555727, - -0.0016 - ], - [ - 0.043786474508437576, - -0.0012 - ], - [ - 0.04411832212156129, - -0.0008 - ], - [ - 0.044536474508437576, - -0.0004 - ], - [ - 0.045, - 0.0 - ], - [ - 0.04546352549156242, - 0.0004 - ], - [ - 0.045881677878438706, - 0.0008 - ], - [ - 0.04621352549156242, - 0.0012 - ], - [ - 0.046426584774442725, - 0.0016 - ], - [ - 0.0465, - 0.002 - ], - [ - 0.04642658477444273, - 0.0024 - ], - [ - 0.04621352549156242, - 0.0028 - ], - [ - 0.045881677878438706, - 0.0032 - ], - [ - 0.04546352549156242, - 0.0036 - ], - [ - 0.045, - 0.004 - ], - [ - 0.044536474508437576, - 0.0044 - ], - [ - 0.04411832212156129, - 0.0048 - ], - [ - 0.043786474508437576, - 0.0052 - ], - [ - 0.04357341522555727, - 0.0056 - ], - [ - 0.0435, - 0.006 - ] - ] - ], - "min_separation_m": 0.0003, - "branch_angle_rad": 0.5235987755982988, - "position_jitter_m": 0.00015, - "squiggle_amplitude_m": 0.0015, - "anchors": [ - { - "segments_m": [ - ], - "anchor_m": [ - 0.045, - 0.0 - ], - "source_count": 21, - "centerlines_m": [ - [ - [ - 0.0465, - -0.006 - ], - [ - 0.046426584774442725, - -0.0056 - ], - [ - 0.04621352549156242, - -0.0052 - ], - [ - 0.045881677878438706, - -0.0048 - ], - [ - 0.04546352549156242, - -0.0044 - ], - [ - 0.045, - -0.004 - ], - [ - 0.044536474508437576, - -0.0036 - ], - [ - 0.04411832212156129, - -0.0032 - ], - [ - 0.043786474508437576, - -0.0028 - ], - [ - 0.043573415225557265, - -0.0024 - ], - [ - 0.0435, - -0.002 - ], - [ - 0.04357341522555727, - -0.0016 - ], - [ - 0.043786474508437576, - -0.0012 - ], - [ - 0.04411832212156129, - -0.0008 - ], - [ - 0.044536474508437576, - -0.0004 - ], - [ - 0.045, - 0.0 - ], - [ - 0.04546352549156242, - 0.0004 - ], - [ - 0.045881677878438706, - 0.0008 - ], - [ - 0.04621352549156242, - 0.0012 - ], - [ - 0.046426584774442725, - 0.0016 - ], - [ - 0.0465, - 0.002 - ], - [ - 0.04642658477444273, - 0.0024 - ], - [ - 0.04621352549156242, - 0.0028 - ], - [ - 0.045881677878438706, - 0.0032 - ], - [ - 0.04546352549156242, - 0.0036 - ], - [ - 0.045, - 0.004 - ], - [ - 0.044536474508437576, - 0.0044 - ], - [ - 0.04411832212156129, - 0.0048 - ], - [ - 0.043786474508437576, - 0.0052 - ], - [ - 0.04357341522555727, - 0.0056 - ], - [ - 0.0435, - 0.006 - ] - ] - ] - } - ], - "branch_scale": 0.65, - "topology": "squiggle", - "max_sources_per_anchor": 0, - "truth_radius_m": 0.001, - "squiggle_wavelength_m": 0.008, - "length_m": 0.012, - "squiggle_slope": 0, - "bundle_spacing_m": 0.002 - }, - "n_anchor_clusters": 1, - "physical_source_count": 21, - "anchor_clusters_m": [ - [ - 0.045, - 0.0 - ] - ] - }, - "n_realizations": 1, - "activity_model": { - "emission_event_count": 21, - "activity_mode": "static", - "phase_jitter_rad": 0, - "active_probability": 1, - "physical_source_count": 21, - "hop_s": null, - "amplitude_jitter": 0, - "frame_duration_s": null - }, - "detection_truth_radius_m": 0.001, - "boundary_threshold_ratios": [ - 0.6, - 0.65, - 0.7 - ], - "simulation": { - "receiver_row": 1, - "source_indices": [ - [ - 233, - 227 - ], - [ - 232, - 231 - ], - [ - 230, - 233 - ], - [ - 227, - 236 - ], - [ - 224, - 239 - ], - [ - 221, - 241 - ], - [ - 218, - 244 - ], - [ - 219, - 248 - ], - [ - 221, - 251 - ], - [ - 224, - 254 - ], - [ - 226, - 257 - ], - [ - 229, - 259 - ], - [ - 231, - 263 - ], - [ - 234, - 266 - ], - [ - 233, - 269 - ], - [ - 231, - 273 - ], - [ - 229, - 276 - ], - [ - 226, - 279 - ], - [ - 221, - 280 - ], - [ - 219, - 283 - ], - [ - 218, - 287 - ] - ], - "receiver_cols": [ - 7, - 506 - ] - }, - "reconstruction_frequencies_hz": [ - 1000000, - 1500000, - 2000000 - ], - "activity_boundary_metrics": { - "geometric": [ - { - "target_centroid_lateral_mm": 2.0204027711162598e-17, - "axial_spread_mm": 23.039685278514263, - "peak_intensity": 1.6894841691673698e6, - "dice": 0.31481968939194527, - "false_positive_area_mm2": 93.84, - "energy_inside_predicted_mask": 3.459917340568953e9, - "overlap_area_mm2": 23.92, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.8990499835303484, - "centroid_lateral_mm": -0.5542203559218081, - "f1": 0.31481968939194527, - "threshold_ratio": 0.6, - "false_positive_pixels": 2346, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.4, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.99999999999995, - "true_negative_pixels": 201599, - "energy_outside_predicted_mask": 3.0813651516243477e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 3.427356885681243e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 0, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899959, - "jaccard": 0.18681661980631054, - "psf_target_correlation": 0.284469648900298, - "psf_target_ssim_like": 0.24635239694466196, - "precision": 0.203125, - "peak_mm": [ - 40.4, - -0.8 - ], - "energy_fraction_inside_predicted_mask": 0.1009500164696516, - "recovered_truth_components": 1, - "lateral_spread_mm": 14.2823287871535, - "true_positive_pixels": 598, - "centroid_error_mm": 5.067899499634459, - "recall": 0.6994152046783626, - "matched_prediction_components": 1, - "prediction_components": 1, - "false_negative_pixels": 257, - "energy_fraction_inside_mask": 0.02692962003722765, - "predicted_area_mm2": 117.76, - "energy_fraction_outside_mask": 0.9730703799627723, - "max_intensity": 1.6894841691673698e6, - "energy_inside_mask": 9.229741866337175e8, - "centroid_depth_mm": 39.962496140400724, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8117734004529122, - "psf_target_normalized_l2_error": 0.9746647981267593, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 10.28, - "energy_outside_mask": 3.3350594670178715e10, - "num_truth_sources": 21, - "threshold_db": -2.218487496163564 - }, - { - "target_centroid_lateral_mm": 2.0204027711162598e-17, - "axial_spread_mm": 23.039685278514263, - "peak_intensity": 1.6894841691673698e6, - "dice": 0.3126085446335533, - "false_positive_area_mm2": 62.96, - "energy_inside_predicted_mask": 2.487449681560962e9, - "overlap_area_mm2": 18, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.9274236747286811, - "centroid_lateral_mm": -0.5542203559218081, - "f1": 0.31260854463355325, - "threshold_ratio": 0.65, - "false_positive_pixels": 1574, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.4, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.99999999999995, - "true_negative_pixels": 202371, - "energy_outside_predicted_mask": 3.178611917525147e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 3.427356885681243e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 2, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899959, - "jaccard": 0.18526142445450802, - "psf_target_correlation": 0.284469648900298, - "psf_target_ssim_like": 0.24635239694466196, - "precision": 0.22233201581027667, - "peak_mm": [ - 40.4, - -0.8 - ], - "energy_fraction_inside_predicted_mask": 0.07257632527131884, - "recovered_truth_components": 1, - "lateral_spread_mm": 14.2823287871535, - "true_positive_pixels": 450, - "centroid_error_mm": 5.067899499634459, - "recall": 0.5263157894736842, - "matched_prediction_components": 2, - "prediction_components": 4, - "false_negative_pixels": 405, - "energy_fraction_inside_mask": 0.02692962003722765, - "predicted_area_mm2": 80.96000000000001, - "energy_fraction_outside_mask": 0.9730703799627723, - "max_intensity": 1.6894841691673698e6, - "energy_inside_mask": 9.229741866337175e8, - "centroid_depth_mm": 39.962496140400724, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8117734004529122, - "psf_target_normalized_l2_error": 0.9746647981267593, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 16.2, - "energy_outside_mask": 3.3350594670178715e10, - "num_truth_sources": 21, - "threshold_db": -1.8708664335714442 - }, - { - "target_centroid_lateral_mm": 2.0204027711162598e-17, - "axial_spread_mm": 23.039685278514263, - "peak_intensity": 1.6894841691673698e6, - "dice": 0.31620166421928536, - "false_positive_area_mm2": 34.6, - "energy_inside_predicted_mask": 1.5345643659743552e9, - "overlap_area_mm2": 12.92, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.955226011846463, - "centroid_lateral_mm": -0.5542203559218081, - "f1": 0.3162016642192854, - "threshold_ratio": 0.7, - "false_positive_pixels": 865, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.4, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.99999999999995, - "true_negative_pixels": 203080, - "energy_outside_predicted_mask": 3.2739004490838078e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 3.427356885681243e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 5, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899959, - "jaccard": 0.1877906976744186, - "psf_target_correlation": 0.284469648900298, - "psf_target_ssim_like": 0.24635239694466196, - "precision": 0.2718855218855219, - "peak_mm": [ - 40.4, - -0.8 - ], - "energy_fraction_inside_predicted_mask": 0.044773988153537024, - "recovered_truth_components": 1, - "lateral_spread_mm": 14.2823287871535, - "true_positive_pixels": 323, - "centroid_error_mm": 5.067899499634459, - "recall": 0.37777777777777777, - "matched_prediction_components": 2, - "prediction_components": 7, - "false_negative_pixels": 532, - "energy_fraction_inside_mask": 0.02692962003722765, - "predicted_area_mm2": 47.52, - "energy_fraction_outside_mask": 0.9730703799627723, - "max_intensity": 1.6894841691673698e6, - "energy_inside_mask": 9.229741866337175e8, - "centroid_depth_mm": 39.962496140400724, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8117734004529122, - "psf_target_normalized_l2_error": 0.9746647981267593, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 21.28, - "energy_outside_mask": 3.3350594670178715e10, - "num_truth_sources": 21, - "threshold_db": -1.5490195998574319 - } - ], - "hasa": [ - { - "target_centroid_lateral_mm": 2.0204027711162598e-17, - "axial_spread_mm": 22.162172739723236, - "peak_intensity": 3.2973654880032944e6, - "dice": 0.3954933954933955, - "false_positive_area_mm2": 48.4, - "energy_inside_predicted_mask": 3.957335415653137e9, - "overlap_area_mm2": 20.36, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.9213250425063296, - "centroid_lateral_mm": -1.255661094860499, - "f1": 0.39549339549339546, - "threshold_ratio": 0.6, - "false_positive_pixels": 1210, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.4, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.99999999999995, - "true_negative_pixels": 202735, - "energy_outside_predicted_mask": 4.634247460930324e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 5.0299810024956375e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 3, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899959, - "jaccard": 0.24648910411622277, - "psf_target_correlation": 0.3321059725982636, - "psf_target_ssim_like": 0.26140716514094037, - "precision": 0.2961023851076207, - "peak_mm": [ - 44, - -3 - ], - "energy_fraction_inside_predicted_mask": 0.07867495749367036, - "recovered_truth_components": 1, - "lateral_spread_mm": 13.246029736744822, - "true_positive_pixels": 509, - "centroid_error_mm": 1.2868821478349797, - "recall": 0.5953216374269006, - "matched_prediction_components": 2, - "prediction_components": 5, - "false_negative_pixels": 346, - "energy_fraction_inside_mask": 0.03534609002183917, - "predicted_area_mm2": 68.76, - "energy_fraction_outside_mask": 0.9646539099781608, - "max_intensity": 3.2973654880032944e6, - "energy_inside_mask": 1.7779016132235167e9, - "centroid_depth_mm": 45.28174612201426, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8117734004529122, - "psf_target_normalized_l2_error": 0.9661709715544975, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 13.84, - "energy_outside_mask": 4.852190841173286e10, - "num_truth_sources": 21, - "threshold_db": -2.218487496163564 - }, - { - "target_centroid_lateral_mm": 2.0204027711162598e-17, - "axial_spread_mm": 22.162172739723236, - "peak_intensity": 3.2973654880032944e6, - "dice": 0.4281217208814271, - "false_positive_area_mm2": 25.72, - "energy_inside_predicted_mask": 2.5878833212257586e9, - "overlap_area_mm2": 16.32, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.9485508330957557, - "centroid_lateral_mm": -1.255661094860499, - "f1": 0.4281217208814271, - "threshold_ratio": 0.65, - "false_positive_pixels": 643, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.4, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.99999999999995, - "true_negative_pixels": 203302, - "energy_outside_predicted_mask": 4.771192670373061e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 5.0299810024956375e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 0, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899959, - "jaccard": 0.27236315086782376, - "psf_target_correlation": 0.3321059725982636, - "psf_target_ssim_like": 0.26140716514094037, - "precision": 0.3882017126546147, - "peak_mm": [ - 44, - -3 - ], - "energy_fraction_inside_predicted_mask": 0.051449166904244246, - "recovered_truth_components": 1, - "lateral_spread_mm": 13.246029736744822, - "true_positive_pixels": 408, - "centroid_error_mm": 1.2868821478349797, - "recall": 0.47719298245614034, - "matched_prediction_components": 3, - "prediction_components": 3, - "false_negative_pixels": 447, - "energy_fraction_inside_mask": 0.03534609002183917, - "predicted_area_mm2": 42.04, - "energy_fraction_outside_mask": 0.9646539099781608, - "max_intensity": 3.2973654880032944e6, - "energy_inside_mask": 1.7779016132235167e9, - "centroid_depth_mm": 45.28174612201426, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8117734004529122, - "psf_target_normalized_l2_error": 0.9661709715544975, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 17.88, - "energy_outside_mask": 4.852190841173286e10, - "num_truth_sources": 21, - "threshold_db": -1.8708664335714442 - }, - { - "target_centroid_lateral_mm": 2.0204027711162598e-17, - "axial_spread_mm": 22.162172739723236, - "peak_intensity": 3.2973654880032944e6, - "dice": 0.358974358974359, - "false_positive_area_mm2": 14.44, - "energy_inside_predicted_mask": 1.6464135820005667e9, - "overlap_area_mm2": 10.64, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.9672679960185995, - "centroid_lateral_mm": -1.255661094860499, - "f1": 0.3589743589743589, - "threshold_ratio": 0.7, - "false_positive_pixels": 361, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.4, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.99999999999995, - "true_negative_pixels": 203584, - "energy_outside_predicted_mask": 4.865339644295581e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 5.0299810024956375e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 2, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899959, - "jaccard": 0.21875, - "psf_target_correlation": 0.3321059725982636, - "psf_target_ssim_like": 0.26140716514094037, - "precision": 0.42424242424242425, - "peak_mm": [ - 44, - -3 - ], - "energy_fraction_inside_predicted_mask": 0.032732003981400616, - "recovered_truth_components": 1, - "lateral_spread_mm": 13.246029736744822, - "true_positive_pixels": 266, - "centroid_error_mm": 1.2868821478349797, - "recall": 0.3111111111111111, - "matched_prediction_components": 3, - "prediction_components": 5, - "false_negative_pixels": 589, - "energy_fraction_inside_mask": 0.03534609002183917, - "predicted_area_mm2": 25.080000000000002, - "energy_fraction_outside_mask": 0.9646539099781608, - "max_intensity": 3.2973654880032944e6, - "energy_inside_mask": 1.7779016132235167e9, - "centroid_depth_mm": 45.28174612201426, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8117734004529122, - "psf_target_normalized_l2_error": 0.9661709715544975, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 23.56, - "energy_outside_mask": 4.852190841173286e10, - "num_truth_sources": 21, - "threshold_db": -1.5490195998574319 - } - ], - "threshold_ratios": [ - 0.6, - 0.65, - 0.7 - ] - } -} \ No newline at end of file diff --git a/runs/20260506_103348_pam_aperture_sweep/03_aperture_ap100_dropout0p0_seed42/status.json b/runs/20260506_103348_pam_aperture_sweep/03_aperture_ap100_dropout0p0_seed42/status.json deleted file mode 100644 index 27a7d66..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/03_aperture_ap100_dropout0p0_seed42/status.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "best_hasa_f1": 0.4281217208814271, - "best_hasa_recall": 0.47719298245614034, - "hasa_psf_corr": 0.3321059725982636, - "best_geo_threshold": 0.7, - "hasa_centroid_error_mm": 1.2868821478349797, - "best_hasa_jaccard": 0.27236315086782376, - "summary_found": true, - "kind": "simulation", - "best_hasa_precision": 0.3882017126546147, - "status": "success", - "elapsed_min": 6.34, - "id": "aperture_ap100_dropout0p0_seed42", - "best_geo_f1": 0.3162016642192854, - "cmd": "`/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/03_aperture_ap100_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=100 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle`", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/03_aperture_ap100_dropout0p0_seed42/run", - "hasa_psf_l2": 0.9661709715544975, - "best_hasa_threshold": 0.65 -} diff --git a/runs/20260506_103348_pam_aperture_sweep/04_aperture_apfull_dropout0p0_seed42/run.log b/runs/20260506_103348_pam_aperture_sweep/04_aperture_apfull_dropout0p0_seed42/run.log deleted file mode 100644 index 87a83f7..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/04_aperture_apfull_dropout0p0_seed42/run.log +++ /dev/null @@ -1,81 +0,0 @@ -# 2026-05-06T10:52:59.786 -# `/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/04_aperture_apfull_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=full --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle` -WARNING:root:Highest prime factors in each dimension are [11 23] -WARNING:root:Use dimension sizes with lower prime factors to improve speed -┌───────────────────────────────────────────────────────────────┐ -│ kspaceFirstOrder-OMP v1.3 │ -├───────────────────────────────────────────────────────────────┤ -│ Reading simulation configuration: Done │ -│ Number of CPU threads: 12 │ -│ Processor name: │ -├───────────────────────────────────────────────────────────────┤ -│ Simulation details │ -├───────────────────────────────────────────────────────────────┤ -│ Domain dimensions: 440 x 552 │ -│ Medium type: 2D │ -│ Simulation time steps: 25000 │ -├───────────────────────────────────────────────────────────────┤ -│ Initialization │ -├───────────────────────────────────────────────────────────────┤ -│ Memory allocation: Done │ -│ Data loading: Done │ -│ Elapsed time: 0.01s │ -├───────────────────────────────────────────────────────────────┤ -│ FFT plans creation: Done │ -│ Pre-processing phase: Done │ -│ Elapsed time: 0.02s │ -├───────────────────────────────────────────────────────────────┤ -│ Computational resources │ -├───────────────────────────────────────────────────────────────┤ -│ Current host memory in use: 37396515758MB │ -│ Expected output file size: 48MB │ -├───────────────────────────────────────────────────────────────┤ -│ Simulation │ -├──────────┬────────────────┬──────────────┬────────────────────┤ -│ Progress │ Elapsed time │ Time to go │ Est. finish time │ -├──────────┼────────────────┼──────────────┼────────────────────┤ -│ 0% │ 0.009s │ 114.277s │ 06/05/26 10:55:28 │ -│ 5% │ 4.911s │ 93.160s │ 06/05/26 10:55:12 │ -│ 10% │ 9.784s │ 87.978s │ 06/05/26 10:55:11 │ -│ 15% │ 14.669s │ 83.073s │ 06/05/26 10:55:12 │ -│ 20% │ 19.548s │ 78.152s │ 06/05/26 10:55:12 │ -│ 25% │ 24.429s │ 73.255s │ 06/05/26 10:55:12 │ -│ 30% │ 29.304s │ 68.350s │ 06/05/26 10:55:12 │ -│ 35% │ 34.192s │ 63.477s │ 06/05/26 10:55:12 │ -│ 40% │ 39.060s │ 58.571s │ 06/05/26 10:55:11 │ -│ 45% │ 43.928s │ 53.672s │ 06/05/26 10:55:11 │ -│ 50% │ 48.811s │ 48.796s │ 06/05/26 10:55:11 │ -│ 55% │ 53.689s │ 43.913s │ 06/05/26 10:55:11 │ -│ 60% │ 58.567s │ 39.032s │ 06/05/26 10:55:12 │ -│ 65% │ 63.462s │ 34.160s │ 06/05/26 10:55:12 │ -│ 70% │ 68.684s │ 29.425s │ 06/05/26 10:55:12 │ -│ 75% │ 73.571s │ 24.513s │ 06/05/26 10:55:12 │ -│ 80% │ 79.592s │ 19.888s │ 06/05/26 10:55:13 │ -│ 85% │ 84.529s │ 14.908s │ 06/05/26 10:55:13 │ -│ 90% │ 88.720s │ 9.849s │ 06/05/26 10:55:12 │ -│ 95% │ 92.854s │ 4.879s │ 06/05/26 10:55:11 │ -├──────────┴────────────────┴──────────────┴────────────────────┤ -│ Elapsed time: 97.15s │ -├───────────────────────────────────────────────────────────────┤ -│ Sampled data post-processing: Done │ -│ Elapsed time: 0.00s │ -├───────────────────────────────────────────────────────────────┤ -│ Summary │ -├───────────────────────────────────────────────────────────────┤ -│ Peak memory in use: 6163735480MB │ -├───────────────────────────────────────────────────────────────┤ -│ Total execution time: 97.20s │ -├───────────────────────────────────────────────────────────────┤ -│ End of computation │ -└───────────────────────────────────────────────────────────────┘ - Activating project at `~/INI_code/Julia II` -┌ Info: reading DICOM series -└ dicom_dir = "/Users/vm/INI_code/Ultrasound/DIRU_20240404_human_skull_phase_correction_1_2_(skull_Normal)/DICOM/PAT_0000/STD_0000/SER_0002/OBJ_0001" -┌ Info: cropping ROI -│ index_xyz = (170, 190, 400) -└ size_xyz = (705, 360, 450) -┌ Info: resampling x/y only -│ spacing_x_mm = 0.201171875 -│ spacing_y_mm = 0.201171875 -└ new_spacing_xy_mm = 0.2 -Saved PAM cluster outputs to /Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/04_aperture_apfull_dropout0p0_seed42/run diff --git a/runs/20260506_103348_pam_aperture_sweep/04_aperture_apfull_dropout0p0_seed42/run/activity_boundaries.png b/runs/20260506_103348_pam_aperture_sweep/04_aperture_apfull_dropout0p0_seed42/run/activity_boundaries.png deleted file mode 100644 index d324dcd..0000000 Binary files a/runs/20260506_103348_pam_aperture_sweep/04_aperture_apfull_dropout0p0_seed42/run/activity_boundaries.png and /dev/null differ diff --git a/runs/20260506_103348_pam_aperture_sweep/04_aperture_apfull_dropout0p0_seed42/run/overview.png b/runs/20260506_103348_pam_aperture_sweep/04_aperture_apfull_dropout0p0_seed42/run/overview.png deleted file mode 100644 index bbd70b6..0000000 Binary files a/runs/20260506_103348_pam_aperture_sweep/04_aperture_apfull_dropout0p0_seed42/run/overview.png and /dev/null differ diff --git a/runs/20260506_103348_pam_aperture_sweep/04_aperture_apfull_dropout0p0_seed42/run/summary.json b/runs/20260506_103348_pam_aperture_sweep/04_aperture_apfull_dropout0p0_seed42/run/summary.json deleted file mode 100644 index b1ff662..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/04_aperture_apfull_dropout0p0_seed42/run/summary.json +++ /dev/null @@ -1,2622 +0,0 @@ -{ - "reconstruction_bandwidth_hz": 500000, - "reconstruction_source": { - "mode": "simulation" - }, - "clusters": [ - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0060327725038961015, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -320.691671411612, - -481.03750711741804, - -641.383342823224 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.046321440287638786 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.005272737898667883, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -320.2235738069466, - -480.33536071041993, - -640.4471476138932 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04626569514702229 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0047312694272838695, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -317.8032279388882, - -476.70484190833236, - -635.6064558777764 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04572226666640846 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.004200866487451935, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -315.3467548948123, - -473.0201323422185, - -630.6935097896246 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04516619536230458 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0036565083902456493, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -312.9314759402867, - -469.39721391043014, - -625.8629518805734 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04461734980426383 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0031861889638953003, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -310.20409603501, - -465.3061440525151, - -620.40819207002 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04398719954353102 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.00256652716942303, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -308.0188633679521, - -462.0282950519281, - -616.0377267359042 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04348928418531734 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0017486279796490286, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -308.1907975338834, - -462.2861963008251, - -616.3815950677669 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04355435089147969 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.001125078113947233, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -309.8379271124607, - -464.75689066869114, - -619.6758542249214 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.043959799624315364 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0006028161597729909, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -312.11941106984887, - -468.17911660477336, - -624.2388221396977 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04451058220522799 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -2.401284689886321e-5, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -314.2460812377018, - -471.3691218565527, - -628.4921624754036 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.045020721921306135 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0004173119265126402, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -316.95746044523713, - -475.4361906678557, - -633.9149208904743 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04566686911546983 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0011125489663053104, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -318.6137903276162, - -477.9206854914243, - -637.2275806552324 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04605530266618959 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0017115638060362216, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -321.1407190707501, - -481.7110786061252, - -642.2814381415002 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04664759181635218 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0024823400105268, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -319.9942647843374, - -479.99139717650615, - -639.9885295686748 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.0463526618538167 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0031517892589508424, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -318.5017269206361, - -477.7525903809542, - -637.0034538412722 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04597133599677839 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.003865963263819151, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -316.9332073482421, - -475.3998110223631, - -633.8664146964842 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04556339958483016 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0043121080495665395, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -314.38452223900595, - -471.576783358509, - -628.7690444780119 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04492980070620905 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.004534541578681507, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -310.9262770322773, - -466.389415548416, - -621.8525540645546 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.044089545625208976 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.005237053357696269, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -309.35567932143914, - -464.03351898215874, - -618.7113586428783 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04366731027665283 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.005975114360697463, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -308.32565851717015, - -462.4884877757553, - -616.6513170343403 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.0433644115625137 - } - ], - "source_phase_mode": "random_phase_per_window", - "reconstruction_axial_step_m": 5.0e-5, - "detection_truth_mode": "centerline_tube", - "medium": { - "outer_row": 151, - "receiver_row": 1, - "hu_bone_thr": 200, - "slice_index": 250, - "inner_row": 186, - "aberrator": "skull", - "skull_to_transducer": 0.03, - "ct_path": "/Users/vm/INI_code/Ultrasound/DIRU_20240404_human_skull_phase_correction_1_2_(skull_Normal)/DICOM/PAT_0000/STD_0000/SER_0002/OBJ_0001" - }, - "analysis_mode": "detection", - "detection_threshold_ratio": 0.2, - "clean_threshold_ratio": 0.01, - "physical_source_count": 21, - "reconstruction_mode": "windowed", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/04_aperture_apfull_dropout0p0_seed42/run", - "window_info": { - "geometric": { - "effective_window_duration_s": 2.0e-5, - "used_window_ranges": [ - [ - 1001, - 2000 - ], - [ - 1501, - 2500 - ], - [ - 2001, - 3000 - ], - [ - 2501, - 3500 - ], - [ - 3001, - 4000 - ], - [ - 3501, - 4500 - ], - [ - 4001, - 5000 - ], - [ - 4501, - 5500 - ], - [ - 5001, - 6000 - ], - [ - 5501, - 6500 - ], - [ - 6001, - 7000 - ], - [ - 6501, - 7500 - ], - [ - 7001, - 8000 - ], - [ - 7501, - 8500 - ], - [ - 8001, - 9000 - ], - [ - 8501, - 9500 - ], - [ - 9001, - 10000 - ], - [ - 9501, - 10500 - ], - [ - 10001, - 11000 - ], - [ - 10501, - 11500 - ], - [ - 11001, - 12000 - ], - [ - 11501, - 12500 - ], - [ - 12001, - 13000 - ], - [ - 12501, - 13500 - ], - [ - 13001, - 14000 - ], - [ - 13501, - 14500 - ], - [ - 14001, - 15000 - ], - [ - 14501, - 15500 - ], - [ - 15001, - 16000 - ], - [ - 15501, - 16500 - ], - [ - 16001, - 17000 - ], - [ - 16501, - 17500 - ], - [ - 17001, - 18000 - ], - [ - 17501, - 18500 - ], - [ - 18001, - 19000 - ], - [ - 18501, - 19500 - ], - [ - 19001, - 20000 - ], - [ - 19501, - 20500 - ], - [ - 20001, - 21000 - ], - [ - 20501, - 21500 - ], - [ - 21001, - 22000 - ], - [ - 21501, - 22500 - ], - [ - 22001, - 23000 - ], - [ - 22501, - 23500 - ], - [ - 23001, - 24000 - ], - [ - 23501, - 24500 - ], - [ - 24001, - 25000 - ] - ], - "used_window_count": 47, - "skipped_window_ranges": [ - [ - 1, - 1000 - ], - [ - 501, - 1500 - ] - ], - "window_samples": 1000, - "effective_hop_s": 1.0e-5, - "energy_threshold": 604.7010408375905, - "skipped_window_count": 2, - "accumulation": "intensity", - "total_window_count": 49, - "hop_samples": 500 - }, - "hasa": { - "effective_window_duration_s": 2.0e-5, - "used_window_ranges": [ - [ - 1001, - 2000 - ], - [ - 1501, - 2500 - ], - [ - 2001, - 3000 - ], - [ - 2501, - 3500 - ], - [ - 3001, - 4000 - ], - [ - 3501, - 4500 - ], - [ - 4001, - 5000 - ], - [ - 4501, - 5500 - ], - [ - 5001, - 6000 - ], - [ - 5501, - 6500 - ], - [ - 6001, - 7000 - ], - [ - 6501, - 7500 - ], - [ - 7001, - 8000 - ], - [ - 7501, - 8500 - ], - [ - 8001, - 9000 - ], - [ - 8501, - 9500 - ], - [ - 9001, - 10000 - ], - [ - 9501, - 10500 - ], - [ - 10001, - 11000 - ], - [ - 10501, - 11500 - ], - [ - 11001, - 12000 - ], - [ - 11501, - 12500 - ], - [ - 12001, - 13000 - ], - [ - 12501, - 13500 - ], - [ - 13001, - 14000 - ], - [ - 13501, - 14500 - ], - [ - 14001, - 15000 - ], - [ - 14501, - 15500 - ], - [ - 15001, - 16000 - ], - [ - 15501, - 16500 - ], - [ - 16001, - 17000 - ], - [ - 16501, - 17500 - ], - [ - 17001, - 18000 - ], - [ - 17501, - 18500 - ], - [ - 18001, - 19000 - ], - [ - 18501, - 19500 - ], - [ - 19001, - 20000 - ], - [ - 19501, - 20500 - ], - [ - 20001, - 21000 - ], - [ - 20501, - 21500 - ], - [ - 21001, - 22000 - ], - [ - 21501, - 22500 - ], - [ - 22001, - 23000 - ], - [ - 22501, - 23500 - ], - [ - 23001, - 24000 - ], - [ - 23501, - 24500 - ], - [ - 24001, - 25000 - ] - ], - "used_window_count": 47, - "skipped_window_ranges": [ - [ - 1, - 1000 - ], - [ - 501, - 1500 - ] - ], - "window_samples": 1000, - "effective_hop_s": 1.0e-5, - "energy_threshold": 604.7010408375905, - "skipped_window_count": 2, - "accumulation": "intensity", - "total_window_count": 49, - "hop_samples": 500 - } - }, - "window_config": { - "window_duration_s": 1.9999999999999998e-5, - "enabled": true, - "hop_s": 9.999999999999999e-6, - "min_energy_ratio": 0.001, - "taper": "hann", - "accumulation": "intensity" - }, - "activity_boundary_figure": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/04_aperture_apfull_dropout0p0_seed42/run/activity_boundaries.png", - "reference_sound_speed_m_per_s": 1551.0647917287392, - "emission_event_count": 21, - "hasa": { - "truth_components": 1, - "true_negative_pixels": 183036, - "energy_outside_predicted_mask": 2.5055209003730133e10, - "centroid_depth_mm": 45.27106398187984, - "energy_total": 5.0674743305200966e10, - "false_negative_area_mm2": 0.56, - "centroid_error_mm": 1.2644119391633832, - "target_axial_spread_mm": 1.3028997817899959, - "prediction_components": 4, - "false_negative_pixels": 14, - "psf_target_ssim_like": 0.26113633269496384, - "matched_prediction_components": 1, - "recall": 0.9836257309941521, - "f1": 0.07440831674408319, - "energy_fraction_outside_predicted_mask": 0.4944318879491711, - "psf_target_mode": "provided_mask", - "energy_outside_mask": 4.889157396975447e10, - "energy_fraction_inside_predicted_mask": 0.5055681120508289, - "axial_spread_mm": 22.166654845292253, - "false_positive_area_mm2": 836.36, - "missed_truth_components": 0, - "psf_target_normalized_l2_error": 0.9663008170100353, - "psf_target_correlation": 0.33207505631287954, - "true_positive_pixels": 841, - "target_centroid_depth_mm": 44.99999999999995, - "energy_fraction_outside_mask": 0.964811477688068, - "num_truth_sources": 21, - "recovered_truth_components": 1, - "target_centroid_lateral_mm": 2.0204027711162598e-17, - "psf_axial_fwhm_mm": 0.75, - "energy_inside_mask": 1.783169335446496e9, - "precision": 0.03866666666666667, - "dice": 0.07440831674408317, - "jaccard": 0.038641793787906636, - "centroid_lateral_mm": -1.2350149268840136, - "peak_mm": [ - 44, - -3 - ], - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "predicted_area_mm2": 870, - "overlap_area_mm2": 33.64, - "max_intensity": 3.297114966648673e6, - "threshold_ratio": 0.2, - "false_positive_pixels": 20909, - "threshold_db": -6.9897000433601875, - "truth_mask_mode": "provided", - "energy_inside_predicted_mask": 2.5619534301470833e10, - "truth_radius_mm": 1, - "energy_fraction_inside_mask": 0.0351885223119321, - "psf_target_integral": 854.9999999999999, - "truth_area_mm2": 34.2, - "spurious_prediction_components": 3, - "target_lateral_spread_mm": 3.8117734004529122, - "psf_lateral_fwhm_mm": 0.4, - "lateral_spread_mm": 13.39572659330845, - "peak_intensity": 3.297114966648673e6 - }, - "config": { - "axial_dim": 0.08, - "dt": 2.0e-8, - "bottom_margin": 0.01, - "rho0": 1000, - "success_tolerance": 0.0015, - "dx": 0.0002, - "receiver_aperture": null, - "dz": 0.0002, - "zero_pad_factor": 4, - "transverse_dim": 0.1024, - "t_max": 0.0005, - "c0": 1500, - "peak_suppression_radius": 0.008 - }, - "peak_method": "argmax", - "clean_loop_gain": 0.1, - "geometric": { - "truth_components": 1, - "true_negative_pixels": 171022, - "energy_outside_predicted_mask": 1.5154506081316628e10, - "centroid_depth_mm": 39.96075642551802, - "energy_total": 3.45566350083757e10, - "false_negative_area_mm2": 0, - "centroid_error_mm": 5.067255480253234, - "target_axial_spread_mm": 1.3028997817899959, - "prediction_components": 3, - "false_negative_pixels": 0, - "psf_target_ssim_like": 0.2462514004457868, - "matched_prediction_components": 1, - "recall": 1, - "f1": 0.049374873675396294, - "energy_fraction_outside_predicted_mask": 0.4385411391370583, - "psf_target_mode": "provided_mask", - "energy_outside_mask": 3.3628323347426437e10, - "energy_fraction_inside_predicted_mask": 0.5614588608629417, - "axial_spread_mm": 23.040070668634367, - "false_positive_area_mm2": 1316.92, - "missed_truth_components": 0, - "psf_target_normalized_l2_error": 0.9746824859906816, - "psf_target_correlation": 0.2855802982274401, - "true_positive_pixels": 855, - "target_centroid_depth_mm": 44.99999999999995, - "energy_fraction_outside_mask": 0.9731365145731271, - "num_truth_sources": 21, - "recovered_truth_components": 1, - "target_centroid_lateral_mm": 2.0204027711162598e-17, - "psf_axial_fwhm_mm": 0.75, - "energy_inside_mask": 9.28311660949266e8, - "precision": 0.02531233347149032, - "dice": 0.0493748736753963, - "jaccard": 0.02531233347149032, - "centroid_lateral_mm": -0.5320735843837977, - "peak_mm": [ - 40.4, - -0.8 - ], - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "predicted_area_mm2": 1351.1200000000001, - "overlap_area_mm2": 34.2, - "max_intensity": 1.6872094621236185e6, - "threshold_ratio": 0.2, - "false_positive_pixels": 32923, - "threshold_db": -6.9897000433601875, - "truth_mask_mode": "provided", - "energy_inside_predicted_mask": 1.9402128927059074e10, - "truth_radius_mm": 1, - "energy_fraction_inside_mask": 0.026863485426872884, - "psf_target_integral": 854.9999999999999, - "truth_area_mm2": 34.2, - "spurious_prediction_components": 2, - "target_lateral_spread_mm": 3.8117734004529122, - "psf_lateral_fwhm_mm": 0.4, - "lateral_spread_mm": 14.466520027336294, - "peak_intensity": 1.6872094621236185e6 - }, - "clean_max_iter": 500, - "source_variability": { - "amplitude_distribution": "fixed", - "frequency_jitter_percent": 1, - "amplitude_sigma": 0, - "dropout_probability": 0 - }, - "emission_meta": { - "emission_event_count": 21, - "random_seed": 42, - "harmonics": [ - 2, - 3, - 4 - ], - "phase_jitter_rad": 0.2, - "n_bubbles_per_cluster": [ - 10 - ], - "cluster_model": "vascular", - "phase_mode": "geometric", - "n_emission_sources": 21, - "cavitation_model": "harmonic_cos", - "gate_duration_s": 4.9999999999999996e-5, - "delays_s": [ - 0 - ], - "activity_model": { - "emission_event_count": 21, - "activity_mode": "static", - "phase_jitter_rad": 0, - "active_probability": 1, - "physical_source_count": 21, - "hop_s": null, - "amplitude_jitter": 0, - "frame_duration_s": null - }, - "transducer_m": [ - -0.03, - 0.0 - ], - "fundamental_hz": 500000, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "vascular": { - "branch_levels": 2, - "source_spacing_m": 0.0008, - "bundle_count": 3, - "centerlines_m": [ - [ - [ - 0.0465, - -0.006 - ], - [ - 0.046426584774442725, - -0.0056 - ], - [ - 0.04621352549156242, - -0.0052 - ], - [ - 0.045881677878438706, - -0.0048 - ], - [ - 0.04546352549156242, - -0.0044 - ], - [ - 0.045, - -0.004 - ], - [ - 0.044536474508437576, - -0.0036 - ], - [ - 0.04411832212156129, - -0.0032 - ], - [ - 0.043786474508437576, - -0.0028 - ], - [ - 0.043573415225557265, - -0.0024 - ], - [ - 0.0435, - -0.002 - ], - [ - 0.04357341522555727, - -0.0016 - ], - [ - 0.043786474508437576, - -0.0012 - ], - [ - 0.04411832212156129, - -0.0008 - ], - [ - 0.044536474508437576, - -0.0004 - ], - [ - 0.045, - 0.0 - ], - [ - 0.04546352549156242, - 0.0004 - ], - [ - 0.045881677878438706, - 0.0008 - ], - [ - 0.04621352549156242, - 0.0012 - ], - [ - 0.046426584774442725, - 0.0016 - ], - [ - 0.0465, - 0.002 - ], - [ - 0.04642658477444273, - 0.0024 - ], - [ - 0.04621352549156242, - 0.0028 - ], - [ - 0.045881677878438706, - 0.0032 - ], - [ - 0.04546352549156242, - 0.0036 - ], - [ - 0.045, - 0.004 - ], - [ - 0.044536474508437576, - 0.0044 - ], - [ - 0.04411832212156129, - 0.0048 - ], - [ - 0.043786474508437576, - 0.0052 - ], - [ - 0.04357341522555727, - 0.0056 - ], - [ - 0.0435, - 0.006 - ] - ] - ], - "min_separation_m": 0.0003, - "branch_angle_rad": 0.5235987755982988, - "position_jitter_m": 0.00015, - "squiggle_amplitude_m": 0.0015, - "anchors": [ - { - "segments_m": [ - ], - "anchor_m": [ - 0.045, - 0.0 - ], - "source_count": 21, - "centerlines_m": [ - [ - [ - 0.0465, - -0.006 - ], - [ - 0.046426584774442725, - -0.0056 - ], - [ - 0.04621352549156242, - -0.0052 - ], - [ - 0.045881677878438706, - -0.0048 - ], - [ - 0.04546352549156242, - -0.0044 - ], - [ - 0.045, - -0.004 - ], - [ - 0.044536474508437576, - -0.0036 - ], - [ - 0.04411832212156129, - -0.0032 - ], - [ - 0.043786474508437576, - -0.0028 - ], - [ - 0.043573415225557265, - -0.0024 - ], - [ - 0.0435, - -0.002 - ], - [ - 0.04357341522555727, - -0.0016 - ], - [ - 0.043786474508437576, - -0.0012 - ], - [ - 0.04411832212156129, - -0.0008 - ], - [ - 0.044536474508437576, - -0.0004 - ], - [ - 0.045, - 0.0 - ], - [ - 0.04546352549156242, - 0.0004 - ], - [ - 0.045881677878438706, - 0.0008 - ], - [ - 0.04621352549156242, - 0.0012 - ], - [ - 0.046426584774442725, - 0.0016 - ], - [ - 0.0465, - 0.002 - ], - [ - 0.04642658477444273, - 0.0024 - ], - [ - 0.04621352549156242, - 0.0028 - ], - [ - 0.045881677878438706, - 0.0032 - ], - [ - 0.04546352549156242, - 0.0036 - ], - [ - 0.045, - 0.004 - ], - [ - 0.044536474508437576, - 0.0044 - ], - [ - 0.04411832212156129, - 0.0048 - ], - [ - 0.043786474508437576, - 0.0052 - ], - [ - 0.04357341522555727, - 0.0056 - ], - [ - 0.0435, - 0.006 - ] - ] - ] - } - ], - "branch_scale": 0.65, - "topology": "squiggle", - "max_sources_per_anchor": 0, - "truth_radius_m": 0.001, - "squiggle_wavelength_m": 0.008, - "length_m": 0.012, - "squiggle_slope": 0, - "bundle_spacing_m": 0.002 - }, - "n_anchor_clusters": 1, - "physical_source_count": 21, - "anchor_clusters_m": [ - [ - 0.045, - 0.0 - ] - ] - }, - "n_realizations": 1, - "activity_model": { - "emission_event_count": 21, - "activity_mode": "static", - "phase_jitter_rad": 0, - "active_probability": 1, - "physical_source_count": 21, - "hop_s": null, - "amplitude_jitter": 0, - "frame_duration_s": null - }, - "detection_truth_radius_m": 0.001, - "boundary_threshold_ratios": [ - 0.6, - 0.65, - 0.7 - ], - "simulation": { - "receiver_row": 1, - "source_indices": [ - [ - 233, - 227 - ], - [ - 232, - 231 - ], - [ - 230, - 233 - ], - [ - 227, - 236 - ], - [ - 224, - 239 - ], - [ - 221, - 241 - ], - [ - 218, - 244 - ], - [ - 219, - 248 - ], - [ - 221, - 251 - ], - [ - 224, - 254 - ], - [ - 226, - 257 - ], - [ - 229, - 259 - ], - [ - 231, - 263 - ], - [ - 234, - 266 - ], - [ - 233, - 269 - ], - [ - 231, - 273 - ], - [ - 229, - 276 - ], - [ - 226, - 279 - ], - [ - 221, - 280 - ], - [ - 219, - 283 - ], - [ - 218, - 287 - ] - ], - "receiver_cols": [ - 1, - 512 - ] - }, - "reconstruction_frequencies_hz": [ - 1000000, - 1500000, - 2000000 - ], - "activity_boundary_metrics": { - "geometric": [ - { - "target_centroid_lateral_mm": 2.0204027711162598e-17, - "axial_spread_mm": 23.040070668634367, - "peak_intensity": 1.6872094621236185e6, - "dice": 0.31251619590567503, - "false_positive_area_mm2": 96.04, - "energy_inside_predicted_mask": 3.5369999439474826e9, - "overlap_area_mm2": 24.12, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.8976462857830284, - "centroid_lateral_mm": -0.5320735843837977, - "f1": 0.31251619590567503, - "threshold_ratio": 0.6, - "false_positive_pixels": 2401, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.4, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.99999999999995, - "true_negative_pixels": 201544, - "energy_outside_predicted_mask": 3.101963506442822e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 3.45566350083757e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 0, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899959, - "jaccard": 0.1851965601965602, - "psf_target_correlation": 0.2855802982274401, - "psf_target_ssim_like": 0.2462514004457868, - "precision": 0.2007323568575233, - "peak_mm": [ - 40.4, - -0.8 - ], - "energy_fraction_inside_predicted_mask": 0.1023537142169716, - "recovered_truth_components": 1, - "lateral_spread_mm": 14.466520027336294, - "true_positive_pixels": 603, - "centroid_error_mm": 5.067255480253234, - "recall": 0.7052631578947368, - "matched_prediction_components": 1, - "prediction_components": 1, - "false_negative_pixels": 252, - "energy_fraction_inside_mask": 0.026863485426872884, - "predicted_area_mm2": 120.16, - "energy_fraction_outside_mask": 0.9731365145731271, - "max_intensity": 1.6872094621236185e6, - "energy_inside_mask": 9.28311660949266e8, - "centroid_depth_mm": 39.96075642551802, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8117734004529122, - "psf_target_normalized_l2_error": 0.9746824859906816, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 10.08, - "energy_outside_mask": 3.3628323347426437e10, - "num_truth_sources": 21, - "threshold_db": -2.218487496163564 - }, - { - "target_centroid_lateral_mm": 2.0204027711162598e-17, - "axial_spread_mm": 23.040070668634367, - "peak_intensity": 1.6872094621236185e6, - "dice": 0.31959798994974875, - "false_positive_area_mm2": 66.12, - "energy_inside_predicted_mask": 2.614865090706702e9, - "overlap_area_mm2": 19.080000000000002, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.9243310267312508, - "centroid_lateral_mm": -0.5320735843837977, - "f1": 0.3195979899497487, - "threshold_ratio": 0.65, - "false_positive_pixels": 1653, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.4, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.99999999999995, - "true_negative_pixels": 202292, - "energy_outside_predicted_mask": 3.1941769917669e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 3.45566350083757e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 1, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899959, - "jaccard": 0.1901913875598086, - "psf_target_correlation": 0.2855802982274401, - "psf_target_ssim_like": 0.2462514004457868, - "precision": 0.223943661971831, - "peak_mm": [ - 40.4, - -0.8 - ], - "energy_fraction_inside_predicted_mask": 0.07566897326874916, - "recovered_truth_components": 1, - "lateral_spread_mm": 14.466520027336294, - "true_positive_pixels": 477, - "centroid_error_mm": 5.067255480253234, - "recall": 0.5578947368421052, - "matched_prediction_components": 2, - "prediction_components": 3, - "false_negative_pixels": 378, - "energy_fraction_inside_mask": 0.026863485426872884, - "predicted_area_mm2": 85.2, - "energy_fraction_outside_mask": 0.9731365145731271, - "max_intensity": 1.6872094621236185e6, - "energy_inside_mask": 9.28311660949266e8, - "centroid_depth_mm": 39.96075642551802, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8117734004529122, - "psf_target_normalized_l2_error": 0.9746824859906816, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 15.120000000000001, - "energy_outside_mask": 3.3628323347426437e10, - "num_truth_sources": 21, - "threshold_db": -1.8708664335714442 - }, - { - "target_centroid_lateral_mm": 2.0204027711162598e-17, - "axial_spread_mm": 23.040070668634367, - "peak_intensity": 1.6872094621236185e6, - "dice": 0.3301797540208136, - "false_positive_area_mm2": 36.4, - "energy_inside_predicted_mask": 1.6242942147386606e9, - "overlap_area_mm2": 13.96, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.9529961694955261, - "centroid_lateral_mm": -0.5320735843837977, - "f1": 0.3301797540208136, - "threshold_ratio": 0.7, - "false_positive_pixels": 910, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.4, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.99999999999995, - "true_negative_pixels": 203035, - "energy_outside_predicted_mask": 3.2932340793637043e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 3.45566350083757e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 3, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899959, - "jaccard": 0.19773371104815865, - "psf_target_correlation": 0.2855802982274401, - "psf_target_ssim_like": 0.2462514004457868, - "precision": 0.27720413026211277, - "peak_mm": [ - 40.4, - -0.8 - ], - "energy_fraction_inside_predicted_mask": 0.047003830504473904, - "recovered_truth_components": 1, - "lateral_spread_mm": 14.466520027336294, - "true_positive_pixels": 349, - "centroid_error_mm": 5.067255480253234, - "recall": 0.408187134502924, - "matched_prediction_components": 3, - "prediction_components": 6, - "false_negative_pixels": 506, - "energy_fraction_inside_mask": 0.026863485426872884, - "predicted_area_mm2": 50.36, - "energy_fraction_outside_mask": 0.9731365145731271, - "max_intensity": 1.6872094621236185e6, - "energy_inside_mask": 9.28311660949266e8, - "centroid_depth_mm": 39.96075642551802, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8117734004529122, - "psf_target_normalized_l2_error": 0.9746824859906816, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 20.240000000000002, - "energy_outside_mask": 3.3628323347426437e10, - "num_truth_sources": 21, - "threshold_db": -1.5490195998574319 - } - ], - "hasa": [ - { - "target_centroid_lateral_mm": 2.0204027711162598e-17, - "axial_spread_mm": 22.166654845292253, - "peak_intensity": 3.297114966648673e6, - "dice": 0.3947872748179379, - "false_positive_area_mm2": 49.56, - "energy_inside_predicted_mask": 4.0350740145321693e9, - "overlap_area_mm2": 20.6, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.9203730744084887, - "centroid_lateral_mm": -1.2350149268840136, - "f1": 0.3947872748179379, - "threshold_ratio": 0.6, - "false_positive_pixels": 1239, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.4, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.99999999999995, - "true_negative_pixels": 202706, - "energy_outside_predicted_mask": 4.663966929066879e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 5.0674743305200966e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 3, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899959, - "jaccard": 0.24594078319006685, - "psf_target_correlation": 0.33207505631287954, - "psf_target_ssim_like": 0.26113633269496384, - "precision": 0.2936145952109464, - "peak_mm": [ - 44, - -3 - ], - "energy_fraction_inside_predicted_mask": 0.07962692559151123, - "recovered_truth_components": 1, - "lateral_spread_mm": 13.39572659330845, - "true_positive_pixels": 515, - "centroid_error_mm": 1.2644119391633832, - "recall": 0.6023391812865497, - "matched_prediction_components": 2, - "prediction_components": 5, - "false_negative_pixels": 340, - "energy_fraction_inside_mask": 0.0351885223119321, - "predicted_area_mm2": 70.16, - "energy_fraction_outside_mask": 0.964811477688068, - "max_intensity": 3.297114966648673e6, - "energy_inside_mask": 1.783169335446496e9, - "centroid_depth_mm": 45.27106398187984, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8117734004529122, - "psf_target_normalized_l2_error": 0.9663008170100353, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 13.6, - "energy_outside_mask": 4.889157396975447e10, - "num_truth_sources": 21, - "threshold_db": -2.218487496163564 - }, - { - "target_centroid_lateral_mm": 2.0204027711162598e-17, - "axial_spread_mm": 22.166654845292253, - "peak_intensity": 3.297114966648673e6, - "dice": 0.428125, - "false_positive_area_mm2": 26.16, - "energy_inside_predicted_mask": 2.6225691164935656e9, - "overlap_area_mm2": 16.44, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.948247017242919, - "centroid_lateral_mm": -1.2350149268840136, - "f1": 0.42812500000000003, - "threshold_ratio": 0.65, - "false_positive_pixels": 654, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.4, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.99999999999995, - "true_negative_pixels": 203291, - "energy_outside_predicted_mask": 4.80521741887074e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 5.0674743305200966e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 1, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899959, - "jaccard": 0.27236580516898606, - "psf_target_correlation": 0.33207505631287954, - "psf_target_ssim_like": 0.26113633269496384, - "precision": 0.38591549295774646, - "peak_mm": [ - 44, - -3 - ], - "energy_fraction_inside_predicted_mask": 0.05175298275708089, - "recovered_truth_components": 1, - "lateral_spread_mm": 13.39572659330845, - "true_positive_pixels": 411, - "centroid_error_mm": 1.2644119391633832, - "recall": 0.4807017543859649, - "matched_prediction_components": 3, - "prediction_components": 4, - "false_negative_pixels": 444, - "energy_fraction_inside_mask": 0.0351885223119321, - "predicted_area_mm2": 42.6, - "energy_fraction_outside_mask": 0.964811477688068, - "max_intensity": 3.297114966648673e6, - "energy_inside_mask": 1.783169335446496e9, - "centroid_depth_mm": 45.27106398187984, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8117734004529122, - "psf_target_normalized_l2_error": 0.9663008170100353, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 17.76, - "energy_outside_mask": 4.889157396975447e10, - "num_truth_sources": 21, - "threshold_db": -1.8708664335714442 - }, - { - "target_centroid_lateral_mm": 2.0204027711162598e-17, - "axial_spread_mm": 22.166654845292253, - "peak_intensity": 3.297114966648673e6, - "dice": 0.3597315436241611, - "false_positive_area_mm2": 14.68, - "energy_inside_predicted_mask": 1.667697060756131e9, - "overlap_area_mm2": 10.72, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.9670901725004896, - "centroid_lateral_mm": -1.2350149268840136, - "f1": 0.3597315436241611, - "threshold_ratio": 0.7, - "false_positive_pixels": 367, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.4, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.99999999999995, - "true_negative_pixels": 203578, - "energy_outside_predicted_mask": 4.900704624444483e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 5.0674743305200966e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 2, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899959, - "jaccard": 0.2193126022913257, - "psf_target_correlation": 0.33207505631287954, - "psf_target_ssim_like": 0.26113633269496384, - "precision": 0.4220472440944882, - "peak_mm": [ - 44, - -3 - ], - "energy_fraction_inside_predicted_mask": 0.03290982749951035, - "recovered_truth_components": 1, - "lateral_spread_mm": 13.39572659330845, - "true_positive_pixels": 268, - "centroid_error_mm": 1.2644119391633832, - "recall": 0.3134502923976608, - "matched_prediction_components": 3, - "prediction_components": 5, - "false_negative_pixels": 587, - "energy_fraction_inside_mask": 0.0351885223119321, - "predicted_area_mm2": 25.400000000000002, - "energy_fraction_outside_mask": 0.964811477688068, - "max_intensity": 3.297114966648673e6, - "energy_inside_mask": 1.783169335446496e9, - "centroid_depth_mm": 45.27106398187984, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8117734004529122, - "psf_target_normalized_l2_error": 0.9663008170100353, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 23.48, - "energy_outside_mask": 4.889157396975447e10, - "num_truth_sources": 21, - "threshold_db": -1.5490195998574319 - } - ], - "threshold_ratios": [ - 0.6, - 0.65, - 0.7 - ] - } -} \ No newline at end of file diff --git a/runs/20260506_103348_pam_aperture_sweep/04_aperture_apfull_dropout0p0_seed42/status.json b/runs/20260506_103348_pam_aperture_sweep/04_aperture_apfull_dropout0p0_seed42/status.json deleted file mode 100644 index 342a67e..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/04_aperture_apfull_dropout0p0_seed42/status.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "best_hasa_f1": 0.42812500000000003, - "best_hasa_recall": 0.4807017543859649, - "hasa_psf_corr": 0.33207505631287954, - "best_geo_threshold": 0.7, - "hasa_centroid_error_mm": 1.2644119391633832, - "best_hasa_jaccard": 0.27236580516898606, - "summary_found": true, - "kind": "simulation", - "best_hasa_precision": 0.38591549295774646, - "status": "success", - "elapsed_min": 4.67, - "id": "aperture_apfull_dropout0p0_seed42", - "best_geo_f1": 0.3301797540208136, - "cmd": "`/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/04_aperture_apfull_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=full --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle`", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/04_aperture_apfull_dropout0p0_seed42/run", - "hasa_psf_l2": 0.9663008170100353, - "best_hasa_threshold": 0.65 -} diff --git a/runs/20260506_103348_pam_aperture_sweep/05_aperture_ap40_dropout0p0_seed42/run.log b/runs/20260506_103348_pam_aperture_sweep/05_aperture_ap40_dropout0p0_seed42/run.log deleted file mode 100644 index 46238f3..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/05_aperture_ap40_dropout0p0_seed42/run.log +++ /dev/null @@ -1,81 +0,0 @@ -# 2026-05-06T10:57:39.949 -# `/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/05_aperture_ap40_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=40 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle` -WARNING:root:Highest prime factors in each dimension are [11 23] -WARNING:root:Use dimension sizes with lower prime factors to improve speed -┌───────────────────────────────────────────────────────────────┐ -│ kspaceFirstOrder-OMP v1.3 │ -├───────────────────────────────────────────────────────────────┤ -│ Reading simulation configuration: Done │ -│ Number of CPU threads: 12 │ -│ Processor name: │ -├───────────────────────────────────────────────────────────────┤ -│ Simulation details │ -├───────────────────────────────────────────────────────────────┤ -│ Domain dimensions: 440 x 552 │ -│ Medium type: 2D │ -│ Simulation time steps: 25000 │ -├───────────────────────────────────────────────────────────────┤ -│ Initialization │ -├───────────────────────────────────────────────────────────────┤ -│ Memory allocation: Done │ -│ Data loading: Done │ -│ Elapsed time: 0.00s │ -├───────────────────────────────────────────────────────────────┤ -│ FFT plans creation: Done │ -│ Pre-processing phase: Done │ -│ Elapsed time: 0.02s │ -├───────────────────────────────────────────────────────────────┤ -│ Computational resources │ -├───────────────────────────────────────────────────────────────┤ -│ Current host memory in use: 33604834222MB │ -│ Expected output file size: 19MB │ -├───────────────────────────────────────────────────────────────┤ -│ Simulation │ -├──────────┬────────────────┬──────────────┬────────────────────┤ -│ Progress │ Elapsed time │ Time to go │ Est. finish time │ -├──────────┼────────────────┼──────────────┼────────────────────┤ -│ 0% │ 0.007s │ 87.606s │ 06/05/26 10:59:29 │ -│ 5% │ 3.656s │ 69.347s │ 06/05/26 10:59:15 │ -│ 10% │ 7.295s │ 65.596s │ 06/05/26 10:59:14 │ -│ 15% │ 10.919s │ 61.837s │ 06/05/26 10:59:14 │ -│ 20% │ 14.536s │ 58.117s │ 06/05/26 10:59:15 │ -│ 25% │ 18.377s │ 55.107s │ 06/05/26 10:59:16 │ -│ 30% │ 22.228s │ 51.845s │ 06/05/26 10:59:15 │ -│ 35% │ 25.976s │ 48.224s │ 06/05/26 10:59:16 │ -│ 40% │ 29.719s │ 44.564s │ 06/05/26 10:59:16 │ -│ 45% │ 33.479s │ 40.905s │ 06/05/26 10:59:16 │ -│ 50% │ 37.220s │ 37.208s │ 06/05/26 10:59:16 │ -│ 55% │ 40.999s │ 33.534s │ 06/05/26 10:59:16 │ -│ 60% │ 44.724s │ 29.806s │ 06/05/26 10:59:16 │ -│ 65% │ 48.446s │ 26.077s │ 06/05/26 10:59:17 │ -│ 70% │ 52.162s │ 22.347s │ 06/05/26 10:59:16 │ -│ 75% │ 55.931s │ 18.636s │ 06/05/26 10:59:16 │ -│ 80% │ 59.664s │ 14.909s │ 06/05/26 10:59:16 │ -│ 85% │ 63.381s │ 11.178s │ 06/05/26 10:59:17 │ -│ 90% │ 67.100s │ 7.449s │ 06/05/26 10:59:16 │ -│ 95% │ 70.820s │ 3.721s │ 06/05/26 10:59:16 │ -├──────────┴────────────────┴──────────────┴────────────────────┤ -│ Elapsed time: 74.54s │ -├───────────────────────────────────────────────────────────────┤ -│ Sampled data post-processing: Done │ -│ Elapsed time: 0.00s │ -├───────────────────────────────────────────────────────────────┤ -│ Summary │ -├───────────────────────────────────────────────────────────────┤ -│ Peak memory in use: 6125167544MB │ -├───────────────────────────────────────────────────────────────┤ -│ Total execution time: 74.58s │ -├───────────────────────────────────────────────────────────────┤ -│ End of computation │ -└───────────────────────────────────────────────────────────────┘ - Activating project at `~/INI_code/Julia II` -┌ Info: reading DICOM series -└ dicom_dir = "/Users/vm/INI_code/Ultrasound/DIRU_20240404_human_skull_phase_correction_1_2_(skull_Normal)/DICOM/PAT_0000/STD_0000/SER_0002/OBJ_0001" -┌ Info: cropping ROI -│ index_xyz = (170, 190, 400) -└ size_xyz = (705, 360, 450) -┌ Info: resampling x/y only -│ spacing_x_mm = 0.201171875 -│ spacing_y_mm = 0.201171875 -└ new_spacing_xy_mm = 0.2 -Saved PAM cluster outputs to /Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/05_aperture_ap40_dropout0p0_seed42/run diff --git a/runs/20260506_103348_pam_aperture_sweep/05_aperture_ap40_dropout0p0_seed42/run/activity_boundaries.png b/runs/20260506_103348_pam_aperture_sweep/05_aperture_ap40_dropout0p0_seed42/run/activity_boundaries.png deleted file mode 100644 index 9932451..0000000 Binary files a/runs/20260506_103348_pam_aperture_sweep/05_aperture_ap40_dropout0p0_seed42/run/activity_boundaries.png and /dev/null differ diff --git a/runs/20260506_103348_pam_aperture_sweep/05_aperture_ap40_dropout0p0_seed42/run/overview.png b/runs/20260506_103348_pam_aperture_sweep/05_aperture_ap40_dropout0p0_seed42/run/overview.png deleted file mode 100644 index d3154cd..0000000 Binary files a/runs/20260506_103348_pam_aperture_sweep/05_aperture_ap40_dropout0p0_seed42/run/overview.png and /dev/null differ diff --git a/runs/20260506_103348_pam_aperture_sweep/05_aperture_ap40_dropout0p0_seed42/run/summary.json b/runs/20260506_103348_pam_aperture_sweep/05_aperture_ap40_dropout0p0_seed42/run/summary.json deleted file mode 100644 index bc812d9..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/05_aperture_ap40_dropout0p0_seed42/run/summary.json +++ /dev/null @@ -1,2622 +0,0 @@ -{ - "reconstruction_bandwidth_hz": 500000, - "reconstruction_source": { - "mode": "simulation" - }, - "clusters": [ - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0060327725038961015, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -320.691671411612, - -481.03750711741804, - -641.383342823224 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.046321440287638786 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.005272737898667883, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -320.2235738069466, - -480.33536071041993, - -640.4471476138932 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04626569514702229 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0047312694272838695, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -317.8032279388882, - -476.70484190833236, - -635.6064558777764 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04572226666640846 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.004200866487451935, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -315.3467548948123, - -473.0201323422185, - -630.6935097896246 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04516619536230458 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0036565083902456493, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -312.9314759402867, - -469.39721391043014, - -625.8629518805734 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04461734980426383 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0031861889638953003, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -310.20409603501, - -465.3061440525151, - -620.40819207002 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04398719954353102 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.00256652716942303, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -308.0188633679521, - -462.0282950519281, - -616.0377267359042 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04348928418531734 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0017486279796490286, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -308.1907975338834, - -462.2861963008251, - -616.3815950677669 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04355435089147969 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.001125078113947233, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -309.8379271124607, - -464.75689066869114, - -619.6758542249214 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.043959799624315364 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0006028161597729909, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -312.11941106984887, - -468.17911660477336, - -624.2388221396977 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04451058220522799 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -2.401284689886321e-5, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -314.2460812377018, - -471.3691218565527, - -628.4921624754036 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.045020721921306135 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0004173119265126402, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -316.95746044523713, - -475.4361906678557, - -633.9149208904743 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04566686911546983 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0011125489663053104, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -318.6137903276162, - -477.9206854914243, - -637.2275806552324 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04605530266618959 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0017115638060362216, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -321.1407190707501, - -481.7110786061252, - -642.2814381415002 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04664759181635218 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0024823400105268, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -319.9942647843374, - -479.99139717650615, - -639.9885295686748 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.0463526618538167 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0031517892589508424, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -318.5017269206361, - -477.7525903809542, - -637.0034538412722 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04597133599677839 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.003865963263819151, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -316.9332073482421, - -475.3998110223631, - -633.8664146964842 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04556339958483016 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0043121080495665395, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -314.38452223900595, - -471.576783358509, - -628.7690444780119 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04492980070620905 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.004534541578681507, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -310.9262770322773, - -466.389415548416, - -621.8525540645546 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.044089545625208976 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.005237053357696269, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -309.35567932143914, - -464.03351898215874, - -618.7113586428783 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04366731027665283 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.005975114360697463, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -308.32565851717015, - -462.4884877757553, - -616.6513170343403 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.0433644115625137 - } - ], - "source_phase_mode": "random_phase_per_window", - "reconstruction_axial_step_m": 5.0e-5, - "detection_truth_mode": "centerline_tube", - "medium": { - "outer_row": 151, - "receiver_row": 1, - "hu_bone_thr": 200, - "slice_index": 250, - "inner_row": 186, - "aberrator": "skull", - "skull_to_transducer": 0.03, - "ct_path": "/Users/vm/INI_code/Ultrasound/DIRU_20240404_human_skull_phase_correction_1_2_(skull_Normal)/DICOM/PAT_0000/STD_0000/SER_0002/OBJ_0001" - }, - "analysis_mode": "detection", - "detection_threshold_ratio": 0.2, - "clean_threshold_ratio": 0.01, - "physical_source_count": 21, - "reconstruction_mode": "windowed", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/05_aperture_ap40_dropout0p0_seed42/run", - "window_info": { - "geometric": { - "effective_window_duration_s": 2.0e-5, - "used_window_ranges": [ - [ - 501, - 1500 - ], - [ - 1001, - 2000 - ], - [ - 1501, - 2500 - ], - [ - 2001, - 3000 - ], - [ - 2501, - 3500 - ], - [ - 3001, - 4000 - ], - [ - 3501, - 4500 - ], - [ - 4001, - 5000 - ], - [ - 4501, - 5500 - ], - [ - 5001, - 6000 - ], - [ - 5501, - 6500 - ], - [ - 6001, - 7000 - ], - [ - 6501, - 7500 - ], - [ - 7001, - 8000 - ], - [ - 7501, - 8500 - ], - [ - 8001, - 9000 - ], - [ - 8501, - 9500 - ], - [ - 9001, - 10000 - ], - [ - 9501, - 10500 - ], - [ - 10001, - 11000 - ], - [ - 10501, - 11500 - ], - [ - 11001, - 12000 - ], - [ - 11501, - 12500 - ], - [ - 12001, - 13000 - ], - [ - 12501, - 13500 - ], - [ - 13001, - 14000 - ], - [ - 13501, - 14500 - ], - [ - 14001, - 15000 - ], - [ - 14501, - 15500 - ], - [ - 15001, - 16000 - ], - [ - 15501, - 16500 - ], - [ - 16001, - 17000 - ], - [ - 16501, - 17500 - ], - [ - 17001, - 18000 - ], - [ - 17501, - 18500 - ], - [ - 18001, - 19000 - ], - [ - 18501, - 19500 - ], - [ - 19001, - 20000 - ], - [ - 19501, - 20500 - ], - [ - 20001, - 21000 - ], - [ - 20501, - 21500 - ], - [ - 21001, - 22000 - ], - [ - 21501, - 22500 - ], - [ - 22001, - 23000 - ], - [ - 22501, - 23500 - ], - [ - 23001, - 24000 - ], - [ - 23501, - 24500 - ], - [ - 24001, - 25000 - ] - ], - "used_window_count": 48, - "skipped_window_ranges": [ - [ - 1, - 1000 - ] - ], - "window_samples": 1000, - "effective_hop_s": 1.0e-5, - "energy_threshold": 309.3746422314567, - "skipped_window_count": 1, - "accumulation": "intensity", - "total_window_count": 49, - "hop_samples": 500 - }, - "hasa": { - "effective_window_duration_s": 2.0e-5, - "used_window_ranges": [ - [ - 501, - 1500 - ], - [ - 1001, - 2000 - ], - [ - 1501, - 2500 - ], - [ - 2001, - 3000 - ], - [ - 2501, - 3500 - ], - [ - 3001, - 4000 - ], - [ - 3501, - 4500 - ], - [ - 4001, - 5000 - ], - [ - 4501, - 5500 - ], - [ - 5001, - 6000 - ], - [ - 5501, - 6500 - ], - [ - 6001, - 7000 - ], - [ - 6501, - 7500 - ], - [ - 7001, - 8000 - ], - [ - 7501, - 8500 - ], - [ - 8001, - 9000 - ], - [ - 8501, - 9500 - ], - [ - 9001, - 10000 - ], - [ - 9501, - 10500 - ], - [ - 10001, - 11000 - ], - [ - 10501, - 11500 - ], - [ - 11001, - 12000 - ], - [ - 11501, - 12500 - ], - [ - 12001, - 13000 - ], - [ - 12501, - 13500 - ], - [ - 13001, - 14000 - ], - [ - 13501, - 14500 - ], - [ - 14001, - 15000 - ], - [ - 14501, - 15500 - ], - [ - 15001, - 16000 - ], - [ - 15501, - 16500 - ], - [ - 16001, - 17000 - ], - [ - 16501, - 17500 - ], - [ - 17001, - 18000 - ], - [ - 17501, - 18500 - ], - [ - 18001, - 19000 - ], - [ - 18501, - 19500 - ], - [ - 19001, - 20000 - ], - [ - 19501, - 20500 - ], - [ - 20001, - 21000 - ], - [ - 20501, - 21500 - ], - [ - 21001, - 22000 - ], - [ - 21501, - 22500 - ], - [ - 22001, - 23000 - ], - [ - 22501, - 23500 - ], - [ - 23001, - 24000 - ], - [ - 23501, - 24500 - ], - [ - 24001, - 25000 - ] - ], - "used_window_count": 48, - "skipped_window_ranges": [ - [ - 1, - 1000 - ] - ], - "window_samples": 1000, - "effective_hop_s": 1.0e-5, - "energy_threshold": 309.3746422314567, - "skipped_window_count": 1, - "accumulation": "intensity", - "total_window_count": 49, - "hop_samples": 500 - } - }, - "window_config": { - "window_duration_s": 1.9999999999999998e-5, - "enabled": true, - "hop_s": 9.999999999999999e-6, - "min_energy_ratio": 0.001, - "taper": "hann", - "accumulation": "intensity" - }, - "activity_boundary_figure": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/05_aperture_ap40_dropout0p0_seed42/run/activity_boundaries.png", - "reference_sound_speed_m_per_s": 1551.0647917287392, - "emission_event_count": 21, - "hasa": { - "truth_components": 1, - "true_negative_pixels": 177980, - "energy_outside_predicted_mask": 7.825151538066055e9, - "centroid_depth_mm": 45.32520849614009, - "energy_total": 2.532207902815766e10, - "false_negative_area_mm2": 0.64, - "centroid_error_mm": 0.7121510998832777, - "target_axial_spread_mm": 1.3028997817899959, - "prediction_components": 1, - "false_negative_pixels": 16, - "psf_target_ssim_like": 0.2614193334790312, - "matched_prediction_components": 1, - "recall": 0.9812865497076023, - "f1": 0.060667413861672514, - "energy_fraction_outside_predicted_mask": 0.3090248446568956, - "psf_target_mode": "provided_mask", - "energy_outside_mask": 2.4456669396962505e10, - "energy_fraction_inside_predicted_mask": 0.6909751553431044, - "axial_spread_mm": 22.105298277403463, - "false_positive_area_mm2": 1038.6, - "missed_truth_components": 0, - "psf_target_normalized_l2_error": 0.9701594791109309, - "psf_target_correlation": 0.2651437853373778, - "true_positive_pixels": 839, - "target_centroid_depth_mm": 45.000000000000014, - "energy_fraction_outside_mask": 0.9658239108158205, - "num_truth_sources": 21, - "recovered_truth_components": 1, - "target_centroid_lateral_mm": -2.8161469407802647e-17, - "psf_axial_fwhm_mm": 0.75, - "energy_inside_mask": 8.654096311951576e8, - "precision": 0.03130129831368453, - "dice": 0.060667413861672514, - "jaccard": 0.03128262490678598, - "centroid_lateral_mm": -0.6335602758248586, - "peak_mm": [ - 37.400000000000006, - -1.4 - ], - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "predicted_area_mm2": 1072.16, - "overlap_area_mm2": 33.56, - "max_intensity": 1.627050842264734e6, - "threshold_ratio": 0.2, - "false_positive_pixels": 25965, - "threshold_db": -6.9897000433601875, - "truth_mask_mode": "provided", - "energy_inside_predicted_mask": 1.7496927490091606e10, - "truth_radius_mm": 1, - "energy_fraction_inside_mask": 0.034176089184179505, - "psf_target_integral": 854.9999999999998, - "truth_area_mm2": 34.2, - "spurious_prediction_components": 0, - "target_lateral_spread_mm": 3.8154594657193996, - "psf_lateral_fwhm_mm": 0.5625, - "lateral_spread_mm": 8.648938566410571, - "peak_intensity": 1.627050842264734e6 - }, - "config": { - "axial_dim": 0.08, - "dt": 2.0e-8, - "bottom_margin": 0.01, - "rho0": 1000, - "success_tolerance": 0.0015, - "dx": 0.0002, - "receiver_aperture": 0.04, - "dz": 0.0002, - "zero_pad_factor": 4, - "transverse_dim": 0.1024, - "t_max": 0.0005, - "c0": 1500, - "peak_suppression_radius": 0.008 - }, - "peak_method": "argmax", - "clean_loop_gain": 0.1, - "geometric": { - "truth_components": 1, - "true_negative_pixels": 164681, - "energy_outside_predicted_mask": 2.889559935502449e9, - "centroid_depth_mm": 39.980005915971454, - "energy_total": 1.7198234169264977e10, - "false_negative_area_mm2": 2.24, - "centroid_error_mm": 5.021085468296783, - "target_axial_spread_mm": 1.3028997817899959, - "prediction_components": 1, - "false_negative_pixels": 56, - "psf_target_ssim_like": 0.24852763470317574, - "matched_prediction_components": 1, - "recall": 0.9345029239766082, - "f1": 0.039053717190478524, - "energy_fraction_outside_predicted_mask": 0.16801491984952685, - "psf_target_mode": "provided_mask", - "energy_outside_mask": 1.6731685552199003e10, - "energy_fraction_inside_predicted_mask": 0.8319850801504731, - "axial_spread_mm": 23.031795438749782, - "false_positive_area_mm2": 1570.56, - "missed_truth_components": 0, - "psf_target_normalized_l2_error": 0.9777552020680877, - "psf_target_correlation": 0.21596303362564373, - "true_positive_pixels": 799, - "target_centroid_depth_mm": 45.000000000000014, - "energy_fraction_outside_mask": 0.9728722953488014, - "num_truth_sources": 21, - "recovered_truth_components": 1, - "target_centroid_lateral_mm": -2.8161469407802647e-17, - "psf_axial_fwhm_mm": 0.75, - "energy_inside_mask": 4.6654861706597364e8, - "precision": 0.019943588847565086, - "dice": 0.03905371719047852, - "jaccard": 0.019915750641840525, - "centroid_lateral_mm": 0.10468369624432046, - "peak_mm": [ - 39.199999999999996, - -0.4 - ], - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "predicted_area_mm2": 1602.52, - "overlap_area_mm2": 31.96, - "max_intensity": 859484.5017512852, - "threshold_ratio": 0.2, - "false_positive_pixels": 39264, - "threshold_db": -6.9897000433601875, - "truth_mask_mode": "provided", - "energy_inside_predicted_mask": 1.4308674233762527e10, - "truth_radius_mm": 1, - "energy_fraction_inside_mask": 0.027127704651198684, - "psf_target_integral": 854.9999999999998, - "truth_area_mm2": 34.2, - "spurious_prediction_components": 0, - "target_lateral_spread_mm": 3.8154594657193996, - "psf_lateral_fwhm_mm": 0.5625, - "lateral_spread_mm": 8.528046784088135, - "peak_intensity": 859484.5017512852 - }, - "clean_max_iter": 500, - "source_variability": { - "amplitude_distribution": "fixed", - "frequency_jitter_percent": 1, - "amplitude_sigma": 0, - "dropout_probability": 0 - }, - "emission_meta": { - "emission_event_count": 21, - "random_seed": 42, - "harmonics": [ - 2, - 3, - 4 - ], - "phase_jitter_rad": 0.2, - "n_bubbles_per_cluster": [ - 10 - ], - "cluster_model": "vascular", - "phase_mode": "geometric", - "n_emission_sources": 21, - "cavitation_model": "harmonic_cos", - "gate_duration_s": 4.9999999999999996e-5, - "delays_s": [ - 0 - ], - "activity_model": { - "emission_event_count": 21, - "activity_mode": "static", - "phase_jitter_rad": 0, - "active_probability": 1, - "physical_source_count": 21, - "hop_s": null, - "amplitude_jitter": 0, - "frame_duration_s": null - }, - "transducer_m": [ - -0.03, - 0.0 - ], - "fundamental_hz": 500000, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "vascular": { - "branch_levels": 2, - "source_spacing_m": 0.0008, - "bundle_count": 3, - "centerlines_m": [ - [ - [ - 0.0465, - -0.006 - ], - [ - 0.046426584774442725, - -0.0056 - ], - [ - 0.04621352549156242, - -0.0052 - ], - [ - 0.045881677878438706, - -0.0048 - ], - [ - 0.04546352549156242, - -0.0044 - ], - [ - 0.045, - -0.004 - ], - [ - 0.044536474508437576, - -0.0036 - ], - [ - 0.04411832212156129, - -0.0032 - ], - [ - 0.043786474508437576, - -0.0028 - ], - [ - 0.043573415225557265, - -0.0024 - ], - [ - 0.0435, - -0.002 - ], - [ - 0.04357341522555727, - -0.0016 - ], - [ - 0.043786474508437576, - -0.0012 - ], - [ - 0.04411832212156129, - -0.0008 - ], - [ - 0.044536474508437576, - -0.0004 - ], - [ - 0.045, - 0.0 - ], - [ - 0.04546352549156242, - 0.0004 - ], - [ - 0.045881677878438706, - 0.0008 - ], - [ - 0.04621352549156242, - 0.0012 - ], - [ - 0.046426584774442725, - 0.0016 - ], - [ - 0.0465, - 0.002 - ], - [ - 0.04642658477444273, - 0.0024 - ], - [ - 0.04621352549156242, - 0.0028 - ], - [ - 0.045881677878438706, - 0.0032 - ], - [ - 0.04546352549156242, - 0.0036 - ], - [ - 0.045, - 0.004 - ], - [ - 0.044536474508437576, - 0.0044 - ], - [ - 0.04411832212156129, - 0.0048 - ], - [ - 0.043786474508437576, - 0.0052 - ], - [ - 0.04357341522555727, - 0.0056 - ], - [ - 0.0435, - 0.006 - ] - ] - ], - "min_separation_m": 0.0003, - "branch_angle_rad": 0.5235987755982988, - "position_jitter_m": 0.00015, - "squiggle_amplitude_m": 0.0015, - "anchors": [ - { - "segments_m": [ - ], - "anchor_m": [ - 0.045, - 0.0 - ], - "source_count": 21, - "centerlines_m": [ - [ - [ - 0.0465, - -0.006 - ], - [ - 0.046426584774442725, - -0.0056 - ], - [ - 0.04621352549156242, - -0.0052 - ], - [ - 0.045881677878438706, - -0.0048 - ], - [ - 0.04546352549156242, - -0.0044 - ], - [ - 0.045, - -0.004 - ], - [ - 0.044536474508437576, - -0.0036 - ], - [ - 0.04411832212156129, - -0.0032 - ], - [ - 0.043786474508437576, - -0.0028 - ], - [ - 0.043573415225557265, - -0.0024 - ], - [ - 0.0435, - -0.002 - ], - [ - 0.04357341522555727, - -0.0016 - ], - [ - 0.043786474508437576, - -0.0012 - ], - [ - 0.04411832212156129, - -0.0008 - ], - [ - 0.044536474508437576, - -0.0004 - ], - [ - 0.045, - 0.0 - ], - [ - 0.04546352549156242, - 0.0004 - ], - [ - 0.045881677878438706, - 0.0008 - ], - [ - 0.04621352549156242, - 0.0012 - ], - [ - 0.046426584774442725, - 0.0016 - ], - [ - 0.0465, - 0.002 - ], - [ - 0.04642658477444273, - 0.0024 - ], - [ - 0.04621352549156242, - 0.0028 - ], - [ - 0.045881677878438706, - 0.0032 - ], - [ - 0.04546352549156242, - 0.0036 - ], - [ - 0.045, - 0.004 - ], - [ - 0.044536474508437576, - 0.0044 - ], - [ - 0.04411832212156129, - 0.0048 - ], - [ - 0.043786474508437576, - 0.0052 - ], - [ - 0.04357341522555727, - 0.0056 - ], - [ - 0.0435, - 0.006 - ] - ] - ] - } - ], - "branch_scale": 0.65, - "topology": "squiggle", - "max_sources_per_anchor": 0, - "truth_radius_m": 0.001, - "squiggle_wavelength_m": 0.008, - "length_m": 0.012, - "squiggle_slope": 0, - "bundle_spacing_m": 0.002 - }, - "n_anchor_clusters": 1, - "physical_source_count": 21, - "anchor_clusters_m": [ - [ - 0.045, - 0.0 - ] - ] - }, - "n_realizations": 1, - "activity_model": { - "emission_event_count": 21, - "activity_mode": "static", - "phase_jitter_rad": 0, - "active_probability": 1, - "physical_source_count": 21, - "hop_s": null, - "amplitude_jitter": 0, - "frame_duration_s": null - }, - "detection_truth_radius_m": 0.001, - "boundary_threshold_ratios": [ - 0.6, - 0.65, - 0.7 - ], - "simulation": { - "receiver_row": 1, - "source_indices": [ - [ - 233, - 227 - ], - [ - 232, - 231 - ], - [ - 230, - 233 - ], - [ - 227, - 236 - ], - [ - 224, - 239 - ], - [ - 221, - 241 - ], - [ - 218, - 244 - ], - [ - 219, - 248 - ], - [ - 221, - 251 - ], - [ - 224, - 254 - ], - [ - 226, - 257 - ], - [ - 229, - 259 - ], - [ - 231, - 263 - ], - [ - 234, - 266 - ], - [ - 233, - 269 - ], - [ - 231, - 273 - ], - [ - 229, - 276 - ], - [ - 226, - 279 - ], - [ - 221, - 280 - ], - [ - 219, - 283 - ], - [ - 218, - 287 - ] - ], - "receiver_cols": [ - 157, - 356 - ] - }, - "reconstruction_frequencies_hz": [ - 1000000, - 1500000, - 2000000 - ], - "activity_boundary_metrics": { - "geometric": [ - { - "target_centroid_lateral_mm": -2.8161469407802647e-17, - "axial_spread_mm": 23.031795438749782, - "peak_intensity": 859484.5017512852, - "dice": 0.15458436000518738, - "false_positive_area_mm2": 250.4, - "energy_inside_predicted_mask": 4.240369450728945e9, - "overlap_area_mm2": 23.84, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.7534415795833899, - "centroid_lateral_mm": 0.10468369624432046, - "f1": 0.15458436000518738, - "threshold_ratio": 0.6, - "false_positive_pixels": 6260, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.5625, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 45.000000000000014, - "true_negative_pixels": 197685, - "energy_outside_predicted_mask": 1.2957864718536032e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 1.7198234169264977e10, - "psf_target_integral": 854.9999999999998, - "spurious_prediction_components": 1, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899959, - "jaccard": 0.08376669009135629, - "psf_target_correlation": 0.21596303362564373, - "psf_target_ssim_like": 0.24852763470317574, - "precision": 0.08693115519253208, - "peak_mm": [ - 39.199999999999996, - -0.4 - ], - "energy_fraction_inside_predicted_mask": 0.2465584204166102, - "recovered_truth_components": 1, - "lateral_spread_mm": 8.528046784088135, - "true_positive_pixels": 596, - "centroid_error_mm": 5.021085468296783, - "recall": 0.6970760233918128, - "matched_prediction_components": 1, - "prediction_components": 2, - "false_negative_pixels": 259, - "energy_fraction_inside_mask": 0.027127704651198684, - "predicted_area_mm2": 274.24, - "energy_fraction_outside_mask": 0.9728722953488014, - "max_intensity": 859484.5017512852, - "energy_inside_mask": 4.6654861706597364e8, - "centroid_depth_mm": 39.980005915971454, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8154594657193996, - "psf_target_normalized_l2_error": 0.9777552020680877, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 10.36, - "energy_outside_mask": 1.6731685552199003e10, - "num_truth_sources": 21, - "threshold_db": -2.218487496163564 - }, - { - "target_centroid_lateral_mm": -2.8161469407802647e-17, - "axial_spread_mm": 23.031795438749782, - "peak_intensity": 859484.5017512852, - "dice": 0.18927550848882166, - "false_positive_area_mm2": 181.24, - "energy_inside_predicted_mask": 3.293682253292399e9, - "overlap_area_mm2": 22.52, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.8084871841564671, - "centroid_lateral_mm": 0.10468369624432046, - "f1": 0.18927550848882166, - "threshold_ratio": 0.65, - "false_positive_pixels": 4531, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.5625, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 45.000000000000014, - "true_negative_pixels": 199414, - "energy_outside_predicted_mask": 1.3904551915972578e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 1.7198234169264977e10, - "psf_target_integral": 854.9999999999998, - "spurious_prediction_components": 0, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899959, - "jaccard": 0.1045302636464909, - "psf_target_correlation": 0.21596303362564373, - "psf_target_ssim_like": 0.24852763470317574, - "precision": 0.11052218296034551, - "peak_mm": [ - 39.199999999999996, - -0.4 - ], - "energy_fraction_inside_predicted_mask": 0.1915128158435329, - "recovered_truth_components": 1, - "lateral_spread_mm": 8.528046784088135, - "true_positive_pixels": 563, - "centroid_error_mm": 5.021085468296783, - "recall": 0.6584795321637427, - "matched_prediction_components": 1, - "prediction_components": 1, - "false_negative_pixels": 292, - "energy_fraction_inside_mask": 0.027127704651198684, - "predicted_area_mm2": 203.76, - "energy_fraction_outside_mask": 0.9728722953488014, - "max_intensity": 859484.5017512852, - "energy_inside_mask": 4.6654861706597364e8, - "centroid_depth_mm": 39.980005915971454, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8154594657193996, - "psf_target_normalized_l2_error": 0.9777552020680877, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 11.68, - "energy_outside_mask": 1.6731685552199003e10, - "num_truth_sources": 21, - "threshold_db": -1.8708664335714442 - }, - { - "target_centroid_lateral_mm": -2.8161469407802647e-17, - "axial_spread_mm": 23.031795438749782, - "peak_intensity": 859484.5017512852, - "dice": 0.1925061700695535, - "false_positive_area_mm2": 126.92, - "energy_inside_predicted_mask": 2.430256372478352e9, - "overlap_area_mm2": 17.16, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.8586915174802381, - "centroid_lateral_mm": 0.10468369624432046, - "f1": 0.1925061700695535, - "threshold_ratio": 0.7, - "false_positive_pixels": 3173, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.5625, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 45.000000000000014, - "true_negative_pixels": 200772, - "energy_outside_predicted_mask": 1.4767977796786625e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 1.7198234169264977e10, - "psf_target_integral": 854.9999999999998, - "spurious_prediction_components": 0, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899959, - "jaccard": 0.10650446871896722, - "psf_target_correlation": 0.21596303362564373, - "psf_target_ssim_like": 0.24852763470317574, - "precision": 0.11910049972237646, - "peak_mm": [ - 39.199999999999996, - -0.4 - ], - "energy_fraction_inside_predicted_mask": 0.1413084825197619, - "recovered_truth_components": 1, - "lateral_spread_mm": 8.528046784088135, - "true_positive_pixels": 429, - "centroid_error_mm": 5.021085468296783, - "recall": 0.5017543859649123, - "matched_prediction_components": 1, - "prediction_components": 1, - "false_negative_pixels": 426, - "energy_fraction_inside_mask": 0.027127704651198684, - "predicted_area_mm2": 144.08, - "energy_fraction_outside_mask": 0.9728722953488014, - "max_intensity": 859484.5017512852, - "energy_inside_mask": 4.6654861706597364e8, - "centroid_depth_mm": 39.980005915971454, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8154594657193996, - "psf_target_normalized_l2_error": 0.9777552020680877, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 17.04, - "energy_outside_mask": 1.6731685552199003e10, - "num_truth_sources": 21, - "threshold_db": -1.5490195998574319 - } - ], - "hasa": [ - { - "target_centroid_lateral_mm": -2.8161469407802647e-17, - "axial_spread_mm": 22.105298277403463, - "peak_intensity": 1.627050842264734e6, - "dice": 0.23510830324909748, - "false_positive_area_mm2": 122.24000000000001, - "energy_inside_predicted_mask": 4.070576527998672e9, - "overlap_area_mm2": 20.84, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.8392479336521985, - "centroid_lateral_mm": -0.6335602758248586, - "f1": 0.23510830324909743, - "threshold_ratio": 0.6, - "false_positive_pixels": 3056, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.5625, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 45.000000000000014, - "true_negative_pixels": 200889, - "energy_outside_predicted_mask": 2.125150250015899e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 2.532207902815766e10, - "psf_target_integral": 854.9999999999998, - "spurious_prediction_components": 1, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899959, - "jaccard": 0.13321401176169778, - "psf_target_correlation": 0.2651437853373778, - "psf_target_ssim_like": 0.2614193334790312, - "precision": 0.14565278166060944, - "peak_mm": [ - 37.400000000000006, - -1.4 - ], - "energy_fraction_inside_predicted_mask": 0.16075206634780145, - "recovered_truth_components": 1, - "lateral_spread_mm": 8.648938566410571, - "true_positive_pixels": 521, - "centroid_error_mm": 0.7121510998832777, - "recall": 0.6093567251461989, - "matched_prediction_components": 1, - "prediction_components": 2, - "false_negative_pixels": 334, - "energy_fraction_inside_mask": 0.034176089184179505, - "predicted_area_mm2": 143.08, - "energy_fraction_outside_mask": 0.9658239108158205, - "max_intensity": 1.627050842264734e6, - "energy_inside_mask": 8.654096311951576e8, - "centroid_depth_mm": 45.32520849614009, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8154594657193996, - "psf_target_normalized_l2_error": 0.9701594791109309, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 13.36, - "energy_outside_mask": 2.4456669396962505e10, - "num_truth_sources": 21, - "threshold_db": -2.218487496163564 - }, - { - "target_centroid_lateral_mm": -2.8161469407802647e-17, - "axial_spread_mm": 22.105298277403463, - "peak_intensity": 1.627050842264734e6, - "dice": 0.2829581993569132, - "false_positive_area_mm2": 72.60000000000001, - "energy_inside_predicted_mask": 2.7345678995290318e9, - "overlap_area_mm2": 17.6, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.8920085552024285, - "centroid_lateral_mm": -0.6335602758248586, - "f1": 0.28295819935691324, - "threshold_ratio": 0.65, - "false_positive_pixels": 1815, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.5625, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 45.000000000000014, - "true_negative_pixels": 202130, - "energy_outside_predicted_mask": 2.258751112862863e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 2.532207902815766e10, - "psf_target_integral": 854.9999999999998, - "spurious_prediction_components": 1, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899959, - "jaccard": 0.1647940074906367, - "psf_target_correlation": 0.2651437853373778, - "psf_target_ssim_like": 0.2614193334790312, - "precision": 0.1951219512195122, - "peak_mm": [ - 37.400000000000006, - -1.4 - ], - "energy_fraction_inside_predicted_mask": 0.10799144479757153, - "recovered_truth_components": 1, - "lateral_spread_mm": 8.648938566410571, - "true_positive_pixels": 440, - "centroid_error_mm": 0.7121510998832777, - "recall": 0.5146198830409356, - "matched_prediction_components": 2, - "prediction_components": 3, - "false_negative_pixels": 415, - "energy_fraction_inside_mask": 0.034176089184179505, - "predicted_area_mm2": 90.2, - "energy_fraction_outside_mask": 0.9658239108158205, - "max_intensity": 1.627050842264734e6, - "energy_inside_mask": 8.654096311951576e8, - "centroid_depth_mm": 45.32520849614009, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8154594657193996, - "psf_target_normalized_l2_error": 0.9701594791109309, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 16.6, - "energy_outside_mask": 2.4456669396962505e10, - "num_truth_sources": 21, - "threshold_db": -1.8708664335714442 - }, - { - "target_centroid_lateral_mm": -2.8161469407802647e-17, - "axial_spread_mm": 22.105298277403463, - "peak_intensity": 1.627050842264734e6, - "dice": 0.2628992628992629, - "false_positive_area_mm2": 50.64, - "energy_inside_predicted_mask": 2.0015453838990326e9, - "overlap_area_mm2": 12.84, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.9209565146024008, - "centroid_lateral_mm": -0.6335602758248586, - "f1": 0.26289926289926285, - "threshold_ratio": 0.7, - "false_positive_pixels": 1266, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.5625, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 45.000000000000014, - "true_negative_pixels": 202679, - "energy_outside_predicted_mask": 2.332053364425863e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 2.532207902815766e10, - "psf_target_integral": 854.9999999999998, - "spurious_prediction_components": 0, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899959, - "jaccard": 0.15134370579915135, - "psf_target_correlation": 0.2651437853373778, - "psf_target_ssim_like": 0.2614193334790312, - "precision": 0.20226843100189035, - "peak_mm": [ - 37.400000000000006, - -1.4 - ], - "energy_fraction_inside_predicted_mask": 0.07904348539759919, - "recovered_truth_components": 1, - "lateral_spread_mm": 8.648938566410571, - "true_positive_pixels": 321, - "centroid_error_mm": 0.7121510998832777, - "recall": 0.37543859649122807, - "matched_prediction_components": 3, - "prediction_components": 3, - "false_negative_pixels": 534, - "energy_fraction_inside_mask": 0.034176089184179505, - "predicted_area_mm2": 63.480000000000004, - "energy_fraction_outside_mask": 0.9658239108158205, - "max_intensity": 1.627050842264734e6, - "energy_inside_mask": 8.654096311951576e8, - "centroid_depth_mm": 45.32520849614009, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8154594657193996, - "psf_target_normalized_l2_error": 0.9701594791109309, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 21.36, - "energy_outside_mask": 2.4456669396962505e10, - "num_truth_sources": 21, - "threshold_db": -1.5490195998574319 - } - ], - "threshold_ratios": [ - 0.6, - 0.65, - 0.7 - ] - } -} \ No newline at end of file diff --git a/runs/20260506_103348_pam_aperture_sweep/05_aperture_ap40_dropout0p0_seed42/status.json b/runs/20260506_103348_pam_aperture_sweep/05_aperture_ap40_dropout0p0_seed42/status.json deleted file mode 100644 index de48dbf..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/05_aperture_ap40_dropout0p0_seed42/status.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "best_hasa_f1": 0.28295819935691324, - "best_hasa_recall": 0.5146198830409356, - "hasa_psf_corr": 0.2651437853373778, - "best_geo_threshold": 0.7, - "hasa_centroid_error_mm": 0.7121510998832777, - "best_hasa_jaccard": 0.1647940074906367, - "summary_found": true, - "kind": "simulation", - "best_hasa_precision": 0.1951219512195122, - "status": "success", - "elapsed_min": 4.09, - "id": "aperture_ap40_dropout0p0_seed42", - "best_geo_f1": 0.1925061700695535, - "cmd": "`/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/05_aperture_ap40_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=40 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle`", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/05_aperture_ap40_dropout0p0_seed42/run", - "hasa_psf_l2": 0.9701594791109309, - "best_hasa_threshold": 0.65 -} diff --git a/runs/20260506_103348_pam_aperture_sweep/06_aperture_ap30_dropout0p0_seed42/run.log b/runs/20260506_103348_pam_aperture_sweep/06_aperture_ap30_dropout0p0_seed42/run.log deleted file mode 100644 index 202b3e6..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/06_aperture_ap30_dropout0p0_seed42/run.log +++ /dev/null @@ -1,81 +0,0 @@ -# 2026-05-06T11:01:45.071 -# `/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/06_aperture_ap30_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=30 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle` -WARNING:root:Highest prime factors in each dimension are [11 23] -WARNING:root:Use dimension sizes with lower prime factors to improve speed -┌───────────────────────────────────────────────────────────────┐ -│ kspaceFirstOrder-OMP v1.3 │ -├───────────────────────────────────────────────────────────────┤ -│ Reading simulation configuration: Done │ -│ Number of CPU threads: 12 │ -│ Processor name: │ -├───────────────────────────────────────────────────────────────┤ -│ Simulation details │ -├───────────────────────────────────────────────────────────────┤ -│ Domain dimensions: 440 x 552 │ -│ Medium type: 2D │ -│ Simulation time steps: 25000 │ -├───────────────────────────────────────────────────────────────┤ -│ Initialization │ -├───────────────────────────────────────────────────────────────┤ -│ Memory allocation: Done │ -│ Data loading: Done │ -│ Elapsed time: 0.00s │ -├───────────────────────────────────────────────────────────────┤ -│ FFT plans creation: Done │ -│ Pre-processing phase: Done │ -│ Elapsed time: 0.02s │ -├───────────────────────────────────────────────────────────────┤ -│ Computational resources │ -├───────────────────────────────────────────────────────────────┤ -│ Current host memory in use: 45411767726MB │ -│ Expected output file size: 14MB │ -├───────────────────────────────────────────────────────────────┤ -│ Simulation │ -├──────────┬────────────────┬──────────────┬────────────────────┤ -│ Progress │ Elapsed time │ Time to go │ Est. finish time │ -├──────────┼────────────────┼──────────────┼────────────────────┤ -│ 0% │ 0.009s │ 106.290s │ 06/05/26 11:03:53 │ -│ 5% │ 3.699s │ 70.159s │ 06/05/26 11:03:21 │ -│ 10% │ 7.510s │ 67.527s │ 06/05/26 11:03:21 │ -│ 15% │ 11.344s │ 64.241s │ 06/05/26 11:03:22 │ -│ 20% │ 15.059s │ 60.205s │ 06/05/26 11:03:22 │ -│ 25% │ 18.752s │ 56.233s │ 06/05/26 11:03:22 │ -│ 30% │ 22.411s │ 52.272s │ 06/05/26 11:03:21 │ -│ 35% │ 26.093s │ 48.441s │ 06/05/26 11:03:21 │ -│ 40% │ 29.770s │ 44.640s │ 06/05/26 11:03:21 │ -│ 45% │ 33.420s │ 40.833s │ 06/05/26 11:03:20 │ -│ 50% │ 37.064s │ 37.052s │ 06/05/26 11:03:21 │ -│ 55% │ 40.687s │ 33.279s │ 06/05/26 11:03:21 │ -│ 60% │ 44.545s │ 29.687s │ 06/05/26 11:03:20 │ -│ 65% │ 48.388s │ 26.046s │ 06/05/26 11:03:21 │ -│ 70% │ 52.250s │ 22.384s │ 06/05/26 11:03:21 │ -│ 75% │ 56.112s │ 18.696s │ 06/05/26 11:03:21 │ -│ 80% │ 59.917s │ 14.972s │ 06/05/26 11:03:21 │ -│ 85% │ 63.585s │ 11.214s │ 06/05/26 11:03:21 │ -│ 90% │ 67.235s │ 7.464s │ 06/05/26 11:03:21 │ -│ 95% │ 70.873s │ 3.724s │ 06/05/26 11:03:21 │ -├──────────┴────────────────┴──────────────┴────────────────────┤ -│ Elapsed time: 74.70s │ -├───────────────────────────────────────────────────────────────┤ -│ Sampled data post-processing: Done │ -│ Elapsed time: 0.00s │ -├───────────────────────────────────────────────────────────────┤ -│ Summary │ -├───────────────────────────────────────────────────────────────┤ -│ Peak memory in use: 6089188280MB │ -├───────────────────────────────────────────────────────────────┤ -│ Total execution time: 74.74s │ -├───────────────────────────────────────────────────────────────┤ -│ End of computation │ -└───────────────────────────────────────────────────────────────┘ - Activating project at `~/INI_code/Julia II` -┌ Info: reading DICOM series -└ dicom_dir = "/Users/vm/INI_code/Ultrasound/DIRU_20240404_human_skull_phase_correction_1_2_(skull_Normal)/DICOM/PAT_0000/STD_0000/SER_0002/OBJ_0001" -┌ Info: cropping ROI -│ index_xyz = (170, 190, 400) -└ size_xyz = (705, 360, 450) -┌ Info: resampling x/y only -│ spacing_x_mm = 0.201171875 -│ spacing_y_mm = 0.201171875 -└ new_spacing_xy_mm = 0.2 -Saved PAM cluster outputs to /Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/06_aperture_ap30_dropout0p0_seed42/run diff --git a/runs/20260506_103348_pam_aperture_sweep/06_aperture_ap30_dropout0p0_seed42/run/activity_boundaries.png b/runs/20260506_103348_pam_aperture_sweep/06_aperture_ap30_dropout0p0_seed42/run/activity_boundaries.png deleted file mode 100644 index 240869b..0000000 Binary files a/runs/20260506_103348_pam_aperture_sweep/06_aperture_ap30_dropout0p0_seed42/run/activity_boundaries.png and /dev/null differ diff --git a/runs/20260506_103348_pam_aperture_sweep/06_aperture_ap30_dropout0p0_seed42/run/overview.png b/runs/20260506_103348_pam_aperture_sweep/06_aperture_ap30_dropout0p0_seed42/run/overview.png deleted file mode 100644 index ba47f2e..0000000 Binary files a/runs/20260506_103348_pam_aperture_sweep/06_aperture_ap30_dropout0p0_seed42/run/overview.png and /dev/null differ diff --git a/runs/20260506_103348_pam_aperture_sweep/06_aperture_ap30_dropout0p0_seed42/run/summary.json b/runs/20260506_103348_pam_aperture_sweep/06_aperture_ap30_dropout0p0_seed42/run/summary.json deleted file mode 100644 index e1317be..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/06_aperture_ap30_dropout0p0_seed42/run/summary.json +++ /dev/null @@ -1,2622 +0,0 @@ -{ - "reconstruction_bandwidth_hz": 500000, - "reconstruction_source": { - "mode": "simulation" - }, - "clusters": [ - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0060327725038961015, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -320.691671411612, - -481.03750711741804, - -641.383342823224 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.046321440287638786 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.005272737898667883, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -320.2235738069466, - -480.33536071041993, - -640.4471476138932 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04626569514702229 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0047312694272838695, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -317.8032279388882, - -476.70484190833236, - -635.6064558777764 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04572226666640846 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.004200866487451935, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -315.3467548948123, - -473.0201323422185, - -630.6935097896246 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04516619536230458 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0036565083902456493, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -312.9314759402867, - -469.39721391043014, - -625.8629518805734 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04461734980426383 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0031861889638953003, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -310.20409603501, - -465.3061440525151, - -620.40819207002 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04398719954353102 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.00256652716942303, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -308.0188633679521, - -462.0282950519281, - -616.0377267359042 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04348928418531734 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0017486279796490286, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -308.1907975338834, - -462.2861963008251, - -616.3815950677669 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04355435089147969 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.001125078113947233, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -309.8379271124607, - -464.75689066869114, - -619.6758542249214 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.043959799624315364 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0006028161597729909, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -312.11941106984887, - -468.17911660477336, - -624.2388221396977 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04451058220522799 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -2.401284689886321e-5, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -314.2460812377018, - -471.3691218565527, - -628.4921624754036 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.045020721921306135 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0004173119265126402, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -316.95746044523713, - -475.4361906678557, - -633.9149208904743 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04566686911546983 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0011125489663053104, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -318.6137903276162, - -477.9206854914243, - -637.2275806552324 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04605530266618959 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0017115638060362216, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -321.1407190707501, - -481.7110786061252, - -642.2814381415002 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04664759181635218 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0024823400105268, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -319.9942647843374, - -479.99139717650615, - -639.9885295686748 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.0463526618538167 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0031517892589508424, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -318.5017269206361, - -477.7525903809542, - -637.0034538412722 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04597133599677839 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.003865963263819151, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -316.9332073482421, - -475.3998110223631, - -633.8664146964842 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04556339958483016 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0043121080495665395, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -314.38452223900595, - -471.576783358509, - -628.7690444780119 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04492980070620905 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.004534541578681507, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -310.9262770322773, - -466.389415548416, - -621.8525540645546 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.044089545625208976 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.005237053357696269, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -309.35567932143914, - -464.03351898215874, - -618.7113586428783 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04366731027665283 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.005975114360697463, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -308.32565851717015, - -462.4884877757553, - -616.6513170343403 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.0433644115625137 - } - ], - "source_phase_mode": "random_phase_per_window", - "reconstruction_axial_step_m": 5.0e-5, - "detection_truth_mode": "centerline_tube", - "medium": { - "outer_row": 151, - "receiver_row": 1, - "hu_bone_thr": 200, - "slice_index": 250, - "inner_row": 186, - "aberrator": "skull", - "skull_to_transducer": 0.03, - "ct_path": "/Users/vm/INI_code/Ultrasound/DIRU_20240404_human_skull_phase_correction_1_2_(skull_Normal)/DICOM/PAT_0000/STD_0000/SER_0002/OBJ_0001" - }, - "analysis_mode": "detection", - "detection_threshold_ratio": 0.2, - "clean_threshold_ratio": 0.01, - "physical_source_count": 21, - "reconstruction_mode": "windowed", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/06_aperture_ap30_dropout0p0_seed42/run", - "window_info": { - "geometric": { - "effective_window_duration_s": 2.0e-5, - "used_window_ranges": [ - [ - 501, - 1500 - ], - [ - 1001, - 2000 - ], - [ - 1501, - 2500 - ], - [ - 2001, - 3000 - ], - [ - 2501, - 3500 - ], - [ - 3001, - 4000 - ], - [ - 3501, - 4500 - ], - [ - 4001, - 5000 - ], - [ - 4501, - 5500 - ], - [ - 5001, - 6000 - ], - [ - 5501, - 6500 - ], - [ - 6001, - 7000 - ], - [ - 6501, - 7500 - ], - [ - 7001, - 8000 - ], - [ - 7501, - 8500 - ], - [ - 8001, - 9000 - ], - [ - 8501, - 9500 - ], - [ - 9001, - 10000 - ], - [ - 9501, - 10500 - ], - [ - 10001, - 11000 - ], - [ - 10501, - 11500 - ], - [ - 11001, - 12000 - ], - [ - 11501, - 12500 - ], - [ - 12001, - 13000 - ], - [ - 12501, - 13500 - ], - [ - 13001, - 14000 - ], - [ - 13501, - 14500 - ], - [ - 14001, - 15000 - ], - [ - 14501, - 15500 - ], - [ - 15001, - 16000 - ], - [ - 15501, - 16500 - ], - [ - 16001, - 17000 - ], - [ - 16501, - 17500 - ], - [ - 17001, - 18000 - ], - [ - 17501, - 18500 - ], - [ - 18001, - 19000 - ], - [ - 18501, - 19500 - ], - [ - 19001, - 20000 - ], - [ - 19501, - 20500 - ], - [ - 20001, - 21000 - ], - [ - 20501, - 21500 - ], - [ - 21001, - 22000 - ], - [ - 21501, - 22500 - ], - [ - 22001, - 23000 - ], - [ - 22501, - 23500 - ], - [ - 23001, - 24000 - ], - [ - 23501, - 24500 - ], - [ - 24001, - 25000 - ] - ], - "used_window_count": 48, - "skipped_window_ranges": [ - [ - 1, - 1000 - ] - ], - "window_samples": 1000, - "effective_hop_s": 1.0e-5, - "energy_threshold": 235.4949165190307, - "skipped_window_count": 1, - "accumulation": "intensity", - "total_window_count": 49, - "hop_samples": 500 - }, - "hasa": { - "effective_window_duration_s": 2.0e-5, - "used_window_ranges": [ - [ - 501, - 1500 - ], - [ - 1001, - 2000 - ], - [ - 1501, - 2500 - ], - [ - 2001, - 3000 - ], - [ - 2501, - 3500 - ], - [ - 3001, - 4000 - ], - [ - 3501, - 4500 - ], - [ - 4001, - 5000 - ], - [ - 4501, - 5500 - ], - [ - 5001, - 6000 - ], - [ - 5501, - 6500 - ], - [ - 6001, - 7000 - ], - [ - 6501, - 7500 - ], - [ - 7001, - 8000 - ], - [ - 7501, - 8500 - ], - [ - 8001, - 9000 - ], - [ - 8501, - 9500 - ], - [ - 9001, - 10000 - ], - [ - 9501, - 10500 - ], - [ - 10001, - 11000 - ], - [ - 10501, - 11500 - ], - [ - 11001, - 12000 - ], - [ - 11501, - 12500 - ], - [ - 12001, - 13000 - ], - [ - 12501, - 13500 - ], - [ - 13001, - 14000 - ], - [ - 13501, - 14500 - ], - [ - 14001, - 15000 - ], - [ - 14501, - 15500 - ], - [ - 15001, - 16000 - ], - [ - 15501, - 16500 - ], - [ - 16001, - 17000 - ], - [ - 16501, - 17500 - ], - [ - 17001, - 18000 - ], - [ - 17501, - 18500 - ], - [ - 18001, - 19000 - ], - [ - 18501, - 19500 - ], - [ - 19001, - 20000 - ], - [ - 19501, - 20500 - ], - [ - 20001, - 21000 - ], - [ - 20501, - 21500 - ], - [ - 21001, - 22000 - ], - [ - 21501, - 22500 - ], - [ - 22001, - 23000 - ], - [ - 22501, - 23500 - ], - [ - 23001, - 24000 - ], - [ - 23501, - 24500 - ], - [ - 24001, - 25000 - ] - ], - "used_window_count": 48, - "skipped_window_ranges": [ - [ - 1, - 1000 - ] - ], - "window_samples": 1000, - "effective_hop_s": 1.0e-5, - "energy_threshold": 235.4949165190307, - "skipped_window_count": 1, - "accumulation": "intensity", - "total_window_count": 49, - "hop_samples": 500 - } - }, - "window_config": { - "window_duration_s": 1.9999999999999998e-5, - "enabled": true, - "hop_s": 9.999999999999999e-6, - "min_energy_ratio": 0.001, - "taper": "hann", - "accumulation": "intensity" - }, - "activity_boundary_figure": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/06_aperture_ap30_dropout0p0_seed42/run/activity_boundaries.png", - "reference_sound_speed_m_per_s": 1551.0647917287392, - "emission_event_count": 21, - "hasa": { - "truth_components": 1, - "true_negative_pixels": 178690, - "energy_outside_predicted_mask": 4.980108965552706e9, - "centroid_depth_mm": 45.371778497165096, - "energy_total": 1.84643666496055e10, - "false_negative_area_mm2": 1.24, - "centroid_error_mm": 0.7173663523393065, - "target_axial_spread_mm": 1.302899781789996, - "prediction_components": 1, - "false_negative_pixels": 31, - "psf_target_ssim_like": 0.2656397482466627, - "matched_prediction_components": 1, - "recall": 0.9637426900584796, - "f1": 0.06118660429197298, - "energy_fraction_outside_predicted_mask": 0.26971458377420754, - "psf_target_mode": "provided_mask", - "energy_outside_mask": 1.7827934532666645e10, - "energy_fraction_inside_predicted_mask": 0.7302854162257925, - "axial_spread_mm": 22.08215414986493, - "false_positive_area_mm2": 1010.2, - "missed_truth_components": 0, - "psf_target_normalized_l2_error": 0.9704168611999944, - "psf_target_correlation": 0.25496439085812905, - "true_positive_pixels": 824, - "target_centroid_depth_mm": 45.00000000000001, - "energy_fraction_outside_mask": 0.9655318739593783, - "num_truth_sources": 21, - "recovered_truth_components": 1, - "target_centroid_lateral_mm": 6.277643942100964e-17, - "psf_axial_fwhm_mm": 0.75, - "energy_inside_mask": 6.364321169388546e8, - "precision": 0.03159630353924614, - "dice": 0.06118660429197297, - "jaccard": 0.031558789735733436, - "centroid_lateral_mm": -0.6135105806049884, - "peak_mm": [ - 37, - -1.4 - ], - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "predicted_area_mm2": 1043.16, - "overlap_area_mm2": 32.96, - "max_intensity": 1.400805223515595e6, - "threshold_ratio": 0.2, - "false_positive_pixels": 25255, - "threshold_db": -6.9897000433601875, - "truth_mask_mode": "provided", - "energy_inside_predicted_mask": 1.3484257684052794e10, - "truth_radius_mm": 1, - "energy_fraction_inside_mask": 0.03446812604062172, - "psf_target_integral": 854.9999999999997, - "truth_area_mm2": 34.2, - "spurious_prediction_components": 0, - "target_lateral_spread_mm": 3.821203295431189, - "psf_lateral_fwhm_mm": 0.75, - "lateral_spread_mm": 8.018384618172469, - "peak_intensity": 1.400805223515595e6 - }, - "config": { - "axial_dim": 0.08, - "dt": 2.0e-8, - "bottom_margin": 0.01, - "rho0": 1000, - "success_tolerance": 0.0015, - "dx": 0.0002, - "receiver_aperture": 0.03, - "dz": 0.0002, - "zero_pad_factor": 4, - "transverse_dim": 0.1024, - "t_max": 0.0005, - "c0": 1500, - "peak_suppression_radius": 0.008 - }, - "peak_method": "argmax", - "clean_loop_gain": 0.1, - "geometric": { - "truth_components": 1, - "true_negative_pixels": 171440, - "energy_outside_predicted_mask": 1.8881725967821941e9, - "centroid_depth_mm": 39.97664459224315, - "energy_total": 1.2476935070499887e10, - "false_negative_area_mm2": 2.88, - "centroid_error_mm": 5.030092230051321, - "target_axial_spread_mm": 1.302899781789996, - "prediction_components": 1, - "false_negative_pixels": 72, - "psf_target_ssim_like": 0.25371150404251197, - "matched_prediction_components": 1, - "recall": 0.9157894736842105, - "f1": 0.04586591687900887, - "energy_fraction_outside_predicted_mask": 0.15133304662669408, - "psf_target_mode": "provided_mask", - "energy_outside_mask": 1.21250901964608e10, - "energy_fraction_inside_predicted_mask": 0.8486669533733059, - "axial_spread_mm": 23.030724802435035, - "false_positive_area_mm2": 1300.2, - "missed_truth_components": 0, - "psf_target_normalized_l2_error": 0.978260429867803, - "psf_target_correlation": 0.20277381457394095, - "true_positive_pixels": 783, - "target_centroid_depth_mm": 45.00000000000001, - "energy_fraction_outside_mask": 0.9718003762902494, - "num_truth_sources": 21, - "recovered_truth_components": 1, - "target_centroid_lateral_mm": 6.277643942100964e-17, - "psf_axial_fwhm_mm": 0.75, - "energy_inside_mask": 3.5184487403908706e8, - "precision": 0.02352198990627253, - "dice": 0.04586591687900887, - "jaccard": 0.023471223021582735, - "centroid_lateral_mm": 0.26024659494905705, - "peak_mm": [ - 29.8, - -1.2000000000000002 - ], - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "predicted_area_mm2": 1331.52, - "overlap_area_mm2": 31.32, - "max_intensity": 725324.4763628589, - "threshold_ratio": 0.2, - "false_positive_pixels": 32505, - "threshold_db": -6.9897000433601875, - "truth_mask_mode": "provided", - "energy_inside_predicted_mask": 1.0588762473717693e10, - "truth_radius_mm": 1, - "energy_fraction_inside_mask": 0.02819962370975057, - "psf_target_integral": 854.9999999999997, - "truth_area_mm2": 34.2, - "spurious_prediction_components": 0, - "target_lateral_spread_mm": 3.821203295431189, - "psf_lateral_fwhm_mm": 0.75, - "lateral_spread_mm": 7.411837460588131, - "peak_intensity": 725324.4763628589 - }, - "clean_max_iter": 500, - "source_variability": { - "amplitude_distribution": "fixed", - "frequency_jitter_percent": 1, - "amplitude_sigma": 0, - "dropout_probability": 0 - }, - "emission_meta": { - "emission_event_count": 21, - "random_seed": 42, - "harmonics": [ - 2, - 3, - 4 - ], - "phase_jitter_rad": 0.2, - "n_bubbles_per_cluster": [ - 10 - ], - "cluster_model": "vascular", - "phase_mode": "geometric", - "n_emission_sources": 21, - "cavitation_model": "harmonic_cos", - "gate_duration_s": 4.9999999999999996e-5, - "delays_s": [ - 0 - ], - "activity_model": { - "emission_event_count": 21, - "activity_mode": "static", - "phase_jitter_rad": 0, - "active_probability": 1, - "physical_source_count": 21, - "hop_s": null, - "amplitude_jitter": 0, - "frame_duration_s": null - }, - "transducer_m": [ - -0.03, - 0.0 - ], - "fundamental_hz": 500000, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "vascular": { - "branch_levels": 2, - "source_spacing_m": 0.0008, - "bundle_count": 3, - "centerlines_m": [ - [ - [ - 0.0465, - -0.006 - ], - [ - 0.046426584774442725, - -0.0056 - ], - [ - 0.04621352549156242, - -0.0052 - ], - [ - 0.045881677878438706, - -0.0048 - ], - [ - 0.04546352549156242, - -0.0044 - ], - [ - 0.045, - -0.004 - ], - [ - 0.044536474508437576, - -0.0036 - ], - [ - 0.04411832212156129, - -0.0032 - ], - [ - 0.043786474508437576, - -0.0028 - ], - [ - 0.043573415225557265, - -0.0024 - ], - [ - 0.0435, - -0.002 - ], - [ - 0.04357341522555727, - -0.0016 - ], - [ - 0.043786474508437576, - -0.0012 - ], - [ - 0.04411832212156129, - -0.0008 - ], - [ - 0.044536474508437576, - -0.0004 - ], - [ - 0.045, - 0.0 - ], - [ - 0.04546352549156242, - 0.0004 - ], - [ - 0.045881677878438706, - 0.0008 - ], - [ - 0.04621352549156242, - 0.0012 - ], - [ - 0.046426584774442725, - 0.0016 - ], - [ - 0.0465, - 0.002 - ], - [ - 0.04642658477444273, - 0.0024 - ], - [ - 0.04621352549156242, - 0.0028 - ], - [ - 0.045881677878438706, - 0.0032 - ], - [ - 0.04546352549156242, - 0.0036 - ], - [ - 0.045, - 0.004 - ], - [ - 0.044536474508437576, - 0.0044 - ], - [ - 0.04411832212156129, - 0.0048 - ], - [ - 0.043786474508437576, - 0.0052 - ], - [ - 0.04357341522555727, - 0.0056 - ], - [ - 0.0435, - 0.006 - ] - ] - ], - "min_separation_m": 0.0003, - "branch_angle_rad": 0.5235987755982988, - "position_jitter_m": 0.00015, - "squiggle_amplitude_m": 0.0015, - "anchors": [ - { - "segments_m": [ - ], - "anchor_m": [ - 0.045, - 0.0 - ], - "source_count": 21, - "centerlines_m": [ - [ - [ - 0.0465, - -0.006 - ], - [ - 0.046426584774442725, - -0.0056 - ], - [ - 0.04621352549156242, - -0.0052 - ], - [ - 0.045881677878438706, - -0.0048 - ], - [ - 0.04546352549156242, - -0.0044 - ], - [ - 0.045, - -0.004 - ], - [ - 0.044536474508437576, - -0.0036 - ], - [ - 0.04411832212156129, - -0.0032 - ], - [ - 0.043786474508437576, - -0.0028 - ], - [ - 0.043573415225557265, - -0.0024 - ], - [ - 0.0435, - -0.002 - ], - [ - 0.04357341522555727, - -0.0016 - ], - [ - 0.043786474508437576, - -0.0012 - ], - [ - 0.04411832212156129, - -0.0008 - ], - [ - 0.044536474508437576, - -0.0004 - ], - [ - 0.045, - 0.0 - ], - [ - 0.04546352549156242, - 0.0004 - ], - [ - 0.045881677878438706, - 0.0008 - ], - [ - 0.04621352549156242, - 0.0012 - ], - [ - 0.046426584774442725, - 0.0016 - ], - [ - 0.0465, - 0.002 - ], - [ - 0.04642658477444273, - 0.0024 - ], - [ - 0.04621352549156242, - 0.0028 - ], - [ - 0.045881677878438706, - 0.0032 - ], - [ - 0.04546352549156242, - 0.0036 - ], - [ - 0.045, - 0.004 - ], - [ - 0.044536474508437576, - 0.0044 - ], - [ - 0.04411832212156129, - 0.0048 - ], - [ - 0.043786474508437576, - 0.0052 - ], - [ - 0.04357341522555727, - 0.0056 - ], - [ - 0.0435, - 0.006 - ] - ] - ] - } - ], - "branch_scale": 0.65, - "topology": "squiggle", - "max_sources_per_anchor": 0, - "truth_radius_m": 0.001, - "squiggle_wavelength_m": 0.008, - "length_m": 0.012, - "squiggle_slope": 0, - "bundle_spacing_m": 0.002 - }, - "n_anchor_clusters": 1, - "physical_source_count": 21, - "anchor_clusters_m": [ - [ - 0.045, - 0.0 - ] - ] - }, - "n_realizations": 1, - "activity_model": { - "emission_event_count": 21, - "activity_mode": "static", - "phase_jitter_rad": 0, - "active_probability": 1, - "physical_source_count": 21, - "hop_s": null, - "amplitude_jitter": 0, - "frame_duration_s": null - }, - "detection_truth_radius_m": 0.001, - "boundary_threshold_ratios": [ - 0.6, - 0.65, - 0.7 - ], - "simulation": { - "receiver_row": 1, - "source_indices": [ - [ - 233, - 227 - ], - [ - 232, - 231 - ], - [ - 230, - 233 - ], - [ - 227, - 236 - ], - [ - 224, - 239 - ], - [ - 221, - 241 - ], - [ - 218, - 244 - ], - [ - 219, - 248 - ], - [ - 221, - 251 - ], - [ - 224, - 254 - ], - [ - 226, - 257 - ], - [ - 229, - 259 - ], - [ - 231, - 263 - ], - [ - 234, - 266 - ], - [ - 233, - 269 - ], - [ - 231, - 273 - ], - [ - 229, - 276 - ], - [ - 226, - 279 - ], - [ - 221, - 280 - ], - [ - 219, - 283 - ], - [ - 218, - 287 - ] - ], - "receiver_cols": [ - 182, - 331 - ] - }, - "reconstruction_frequencies_hz": [ - 1000000, - 1500000, - 2000000 - ], - "activity_boundary_metrics": { - "geometric": [ - { - "target_centroid_lateral_mm": 6.277643942100964e-17, - "axial_spread_mm": 23.030724802435035, - "peak_intensity": 725324.4763628589, - "dice": 0.15389082462253195, - "false_positive_area_mm2": 220.12, - "energy_inside_predicted_mask": 3.0941955678904514e9, - "overlap_area_mm2": 21.2, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.752006758838853, - "centroid_lateral_mm": 0.26024659494905705, - "f1": 0.15389082462253192, - "threshold_ratio": 0.6, - "false_positive_pixels": 5503, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.75, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 45.00000000000001, - "true_negative_pixels": 198442, - "energy_outside_predicted_mask": 9.382739502609436e9, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 1.2476935070499887e10, - "psf_target_integral": 854.9999999999997, - "spurious_prediction_components": 0, - "truth_components": 1, - "target_axial_spread_mm": 1.302899781789996, - "jaccard": 0.0833595470273671, - "psf_target_correlation": 0.20277381457394095, - "psf_target_ssim_like": 0.25371150404251197, - "precision": 0.08785015746726338, - "peak_mm": [ - 29.8, - -1.2000000000000002 - ], - "energy_fraction_inside_predicted_mask": 0.24799324116114702, - "recovered_truth_components": 1, - "lateral_spread_mm": 7.411837460588131, - "true_positive_pixels": 530, - "centroid_error_mm": 5.030092230051321, - "recall": 0.6198830409356725, - "matched_prediction_components": 1, - "prediction_components": 1, - "false_negative_pixels": 325, - "energy_fraction_inside_mask": 0.02819962370975057, - "predicted_area_mm2": 241.32, - "energy_fraction_outside_mask": 0.9718003762902494, - "max_intensity": 725324.4763628589, - "energy_inside_mask": 3.5184487403908706e8, - "centroid_depth_mm": 39.97664459224315, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.821203295431189, - "psf_target_normalized_l2_error": 0.978260429867803, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 13, - "energy_outside_mask": 1.21250901964608e10, - "num_truth_sources": 21, - "threshold_db": -2.218487496163564 - }, - { - "target_centroid_lateral_mm": 6.277643942100964e-17, - "axial_spread_mm": 23.030724802435035, - "peak_intensity": 725324.4763628589, - "dice": 0.14865996649916247, - "false_positive_area_mm2": 142.64000000000001, - "energy_inside_predicted_mask": 2.1367756776342664e9, - "overlap_area_mm2": 14.200000000000001, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.8287419413853969, - "centroid_lateral_mm": 0.26024659494905705, - "f1": 0.14865996649916247, - "threshold_ratio": 0.65, - "false_positive_pixels": 3566, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.75, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 45.00000000000001, - "true_negative_pixels": 200379, - "energy_outside_predicted_mask": 1.0340159392865622e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 1.2476935070499887e10, - "psf_target_integral": 854.9999999999997, - "spurious_prediction_components": 0, - "truth_components": 1, - "target_axial_spread_mm": 1.302899781789996, - "jaccard": 0.08029857498303551, - "psf_target_correlation": 0.20277381457394095, - "psf_target_ssim_like": 0.25371150404251197, - "precision": 0.09053812802856415, - "peak_mm": [ - 29.8, - -1.2000000000000002 - ], - "energy_fraction_inside_predicted_mask": 0.17125805861460305, - "recovered_truth_components": 1, - "lateral_spread_mm": 7.411837460588131, - "true_positive_pixels": 355, - "centroid_error_mm": 5.030092230051321, - "recall": 0.4152046783625731, - "matched_prediction_components": 1, - "prediction_components": 1, - "false_negative_pixels": 500, - "energy_fraction_inside_mask": 0.02819962370975057, - "predicted_area_mm2": 156.84, - "energy_fraction_outside_mask": 0.9718003762902494, - "max_intensity": 725324.4763628589, - "energy_inside_mask": 3.5184487403908706e8, - "centroid_depth_mm": 39.97664459224315, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.821203295431189, - "psf_target_normalized_l2_error": 0.978260429867803, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 20, - "energy_outside_mask": 1.21250901964608e10, - "num_truth_sources": 21, - "threshold_db": -1.8708664335714442 - }, - { - "target_centroid_lateral_mm": 6.277643942100964e-17, - "axial_spread_mm": 23.030724802435035, - "peak_intensity": 725324.4763628589, - "dice": 0.14208273894436518, - "false_positive_area_mm2": 96.04, - "energy_inside_predicted_mask": 1.5158148740032225e9, - "overlap_area_mm2": 9.96, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.8785106385952772, - "centroid_lateral_mm": 0.26024659494905705, - "f1": 0.1420827389443652, - "threshold_ratio": 0.7, - "false_positive_pixels": 2401, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.75, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 45.00000000000001, - "true_negative_pixels": 201544, - "energy_outside_predicted_mask": 1.0961120196496666e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 1.2476935070499887e10, - "psf_target_integral": 854.9999999999997, - "spurious_prediction_components": 0, - "truth_components": 1, - "target_axial_spread_mm": 1.302899781789996, - "jaccard": 0.07647420147420148, - "psf_target_correlation": 0.20277381457394095, - "psf_target_ssim_like": 0.25371150404251197, - "precision": 0.0939622641509434, - "peak_mm": [ - 29.8, - -1.2000000000000002 - ], - "energy_fraction_inside_predicted_mask": 0.12148936140472288, - "recovered_truth_components": 1, - "lateral_spread_mm": 7.411837460588131, - "true_positive_pixels": 249, - "centroid_error_mm": 5.030092230051321, - "recall": 0.2912280701754386, - "matched_prediction_components": 1, - "prediction_components": 1, - "false_negative_pixels": 606, - "energy_fraction_inside_mask": 0.02819962370975057, - "predicted_area_mm2": 106, - "energy_fraction_outside_mask": 0.9718003762902494, - "max_intensity": 725324.4763628589, - "energy_inside_mask": 3.5184487403908706e8, - "centroid_depth_mm": 39.97664459224315, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.821203295431189, - "psf_target_normalized_l2_error": 0.978260429867803, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 24.240000000000002, - "energy_outside_mask": 1.21250901964608e10, - "num_truth_sources": 21, - "threshold_db": -1.5490195998574319 - } - ], - "hasa": [ - { - "target_centroid_lateral_mm": 6.277643942100964e-17, - "axial_spread_mm": 22.08215414986493, - "peak_intensity": 1.400805223515595e6, - "dice": 0.17551190973673214, - "false_positive_area_mm2": 53.120000000000005, - "energy_inside_predicted_mask": 1.5167856993155127e9, - "overlap_area_mm2": 8.4, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.9178533589535323, - "centroid_lateral_mm": -0.6135105806049884, - "f1": 0.17551190973673214, - "threshold_ratio": 0.6, - "false_positive_pixels": 1328, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.75, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 45.00000000000001, - "true_negative_pixels": 202617, - "energy_outside_predicted_mask": 1.6947580950289986e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 1.84643666496055e10, - "psf_target_integral": 854.9999999999997, - "spurious_prediction_components": 0, - "truth_components": 1, - "target_axial_spread_mm": 1.302899781789996, - "jaccard": 0.0961978928080623, - "psf_target_correlation": 0.25496439085812905, - "psf_target_ssim_like": 0.2656397482466627, - "precision": 0.13654096228868662, - "peak_mm": [ - 37, - -1.4 - ], - "energy_fraction_inside_predicted_mask": 0.08214664104646771, - "recovered_truth_components": 1, - "lateral_spread_mm": 8.018384618172469, - "true_positive_pixels": 210, - "centroid_error_mm": 0.7173663523393065, - "recall": 0.24561403508771928, - "matched_prediction_components": 2, - "prediction_components": 2, - "false_negative_pixels": 645, - "energy_fraction_inside_mask": 0.03446812604062172, - "predicted_area_mm2": 61.52, - "energy_fraction_outside_mask": 0.9655318739593783, - "max_intensity": 1.400805223515595e6, - "energy_inside_mask": 6.364321169388546e8, - "centroid_depth_mm": 45.371778497165096, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.821203295431189, - "psf_target_normalized_l2_error": 0.9704168611999944, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 25.8, - "energy_outside_mask": 1.7827934532666645e10, - "num_truth_sources": 21, - "threshold_db": -2.218487496163564 - }, - { - "target_centroid_lateral_mm": 6.277643942100964e-17, - "axial_spread_mm": 22.08215414986493, - "peak_intensity": 1.400805223515595e6, - "dice": 0.09224091155724362, - "false_positive_area_mm2": 36.12, - "energy_inside_predicted_mask": 1.0356615079049258e9, - "overlap_area_mm2": 3.4, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.9439102609064008, - "centroid_lateral_mm": -0.6135105806049884, - "f1": 0.09224091155724361, - "threshold_ratio": 0.65, - "false_positive_pixels": 903, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.75, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 45.00000000000001, - "true_negative_pixels": 203042, - "energy_outside_predicted_mask": 1.7428705141700573e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 1.84643666496055e10, - "psf_target_integral": 854.9999999999997, - "spurious_prediction_components": 3, - "truth_components": 1, - "target_axial_spread_mm": 1.302899781789996, - "jaccard": 0.04835039817974972, - "psf_target_correlation": 0.25496439085812905, - "psf_target_ssim_like": 0.2656397482466627, - "precision": 0.0860323886639676, - "peak_mm": [ - 37, - -1.4 - ], - "energy_fraction_inside_predicted_mask": 0.05608973909359914, - "recovered_truth_components": 1, - "lateral_spread_mm": 8.018384618172469, - "true_positive_pixels": 85, - "centroid_error_mm": 0.7173663523393065, - "recall": 0.09941520467836257, - "matched_prediction_components": 1, - "prediction_components": 4, - "false_negative_pixels": 770, - "energy_fraction_inside_mask": 0.03446812604062172, - "predicted_area_mm2": 39.52, - "energy_fraction_outside_mask": 0.9655318739593783, - "max_intensity": 1.400805223515595e6, - "energy_inside_mask": 6.364321169388546e8, - "centroid_depth_mm": 45.371778497165096, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.821203295431189, - "psf_target_normalized_l2_error": 0.9704168611999944, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 30.8, - "energy_outside_mask": 1.7827934532666645e10, - "num_truth_sources": 21, - "threshold_db": -1.8708664335714442 - }, - { - "target_centroid_lateral_mm": 6.277643942100964e-17, - "axial_spread_mm": 22.08215414986493, - "peak_intensity": 1.400805223515595e6, - "dice": 0.07312119160460392, - "false_positive_area_mm2": 22.72, - "energy_inside_predicted_mask": 6.897930322219007e8, - "overlap_area_mm2": 2.16, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.9626419337683246, - "centroid_lateral_mm": -0.6135105806049884, - "f1": 0.07312119160460394, - "threshold_ratio": 0.7, - "false_positive_pixels": 568, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.75, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 45.00000000000001, - "true_negative_pixels": 203377, - "energy_outside_predicted_mask": 1.77745736173836e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 1.84643666496055e10, - "psf_target_integral": 854.9999999999997, - "spurious_prediction_components": 3, - "truth_components": 1, - "target_axial_spread_mm": 1.302899781789996, - "jaccard": 0.03794799718903725, - "psf_target_correlation": 0.25496439085812905, - "psf_target_ssim_like": 0.2656397482466627, - "precision": 0.08681672025723473, - "peak_mm": [ - 37, - -1.4 - ], - "energy_fraction_inside_predicted_mask": 0.0373580662316754, - "recovered_truth_components": 1, - "lateral_spread_mm": 8.018384618172469, - "true_positive_pixels": 54, - "centroid_error_mm": 0.7173663523393065, - "recall": 0.06315789473684211, - "matched_prediction_components": 1, - "prediction_components": 4, - "false_negative_pixels": 801, - "energy_fraction_inside_mask": 0.03446812604062172, - "predicted_area_mm2": 24.88, - "energy_fraction_outside_mask": 0.9655318739593783, - "max_intensity": 1.400805223515595e6, - "energy_inside_mask": 6.364321169388546e8, - "centroid_depth_mm": 45.371778497165096, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.821203295431189, - "psf_target_normalized_l2_error": 0.9704168611999944, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 32.04, - "energy_outside_mask": 1.7827934532666645e10, - "num_truth_sources": 21, - "threshold_db": -1.5490195998574319 - } - ], - "threshold_ratios": [ - 0.6, - 0.65, - 0.7 - ] - } -} \ No newline at end of file diff --git a/runs/20260506_103348_pam_aperture_sweep/06_aperture_ap30_dropout0p0_seed42/status.json b/runs/20260506_103348_pam_aperture_sweep/06_aperture_ap30_dropout0p0_seed42/status.json deleted file mode 100644 index 2d26a98..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/06_aperture_ap30_dropout0p0_seed42/status.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "best_hasa_f1": 0.17551190973673214, - "best_hasa_recall": 0.24561403508771928, - "hasa_psf_corr": 0.25496439085812905, - "best_geo_threshold": 0.6, - "hasa_centroid_error_mm": 0.7173663523393065, - "best_hasa_jaccard": 0.0961978928080623, - "summary_found": true, - "kind": "simulation", - "best_hasa_precision": 0.13654096228868662, - "status": "success", - "elapsed_min": 4.09, - "id": "aperture_ap30_dropout0p0_seed42", - "best_geo_f1": 0.15389082462253192, - "cmd": "`/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/06_aperture_ap30_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=30 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle`", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/06_aperture_ap30_dropout0p0_seed42/run", - "hasa_psf_l2": 0.9704168611999944, - "best_hasa_threshold": 0.6 -} diff --git a/runs/20260506_103348_pam_aperture_sweep/07_aperture_ap20_dropout0p0_seed42/run.log b/runs/20260506_103348_pam_aperture_sweep/07_aperture_ap20_dropout0p0_seed42/run.log deleted file mode 100644 index b26ab17..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/07_aperture_ap20_dropout0p0_seed42/run.log +++ /dev/null @@ -1,81 +0,0 @@ -# 2026-05-06T11:05:50.234 -# `/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/07_aperture_ap20_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=20 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle` -WARNING:root:Highest prime factors in each dimension are [11 23] -WARNING:root:Use dimension sizes with lower prime factors to improve speed -┌───────────────────────────────────────────────────────────────┐ -│ kspaceFirstOrder-OMP v1.3 │ -├───────────────────────────────────────────────────────────────┤ -│ Reading simulation configuration: Done │ -│ Number of CPU threads: 12 │ -│ Processor name: │ -├───────────────────────────────────────────────────────────────┤ -│ Simulation details │ -├───────────────────────────────────────────────────────────────┤ -│ Domain dimensions: 440 x 552 │ -│ Medium type: 2D │ -│ Simulation time steps: 25000 │ -├───────────────────────────────────────────────────────────────┤ -│ Initialization │ -├───────────────────────────────────────────────────────────────┤ -│ Memory allocation: Done │ -│ Data loading: Done │ -│ Elapsed time: 0.00s │ -├───────────────────────────────────────────────────────────────┤ -│ FFT plans creation: Done │ -│ Pre-processing phase: Done │ -│ Elapsed time: 0.02s │ -├───────────────────────────────────────────────────────────────┤ -│ Computational resources │ -├───────────────────────────────────────────────────────────────┤ -│ Current host memory in use: 52026248622MB │ -│ Expected output file size: 9MB │ -├───────────────────────────────────────────────────────────────┤ -│ Simulation │ -├──────────┬────────────────┬──────────────┬────────────────────┤ -│ Progress │ Elapsed time │ Time to go │ Est. finish time │ -├──────────┼────────────────┼──────────────┼────────────────────┤ -│ 0% │ 0.006s │ 80.332s │ 06/05/26 11:07:32 │ -│ 5% │ 3.735s │ 70.850s │ 06/05/26 11:07:26 │ -│ 10% │ 7.490s │ 67.354s │ 06/05/26 11:07:27 │ -│ 15% │ 11.211s │ 63.489s │ 06/05/26 11:07:27 │ -│ 20% │ 14.935s │ 59.710s │ 06/05/26 11:07:26 │ -│ 25% │ 18.720s │ 56.137s │ 06/05/26 11:07:27 │ -│ 30% │ 22.608s │ 52.733s │ 06/05/26 11:07:27 │ -│ 35% │ 26.378s │ 48.971s │ 06/05/26 11:07:27 │ -│ 40% │ 30.105s │ 45.142s │ 06/05/26 11:07:28 │ -│ 45% │ 33.858s │ 41.369s │ 06/05/26 11:07:27 │ -│ 50% │ 37.575s │ 37.563s │ 06/05/26 11:07:27 │ -│ 55% │ 41.299s │ 33.779s │ 06/05/26 11:07:27 │ -│ 60% │ 45.017s │ 30.001s │ 06/05/26 11:07:27 │ -│ 65% │ 48.763s │ 26.248s │ 06/05/26 11:07:27 │ -│ 70% │ 52.497s │ 22.490s │ 06/05/26 11:07:27 │ -│ 75% │ 56.237s │ 18.738s │ 06/05/26 11:07:27 │ -│ 80% │ 59.972s │ 14.986s │ 06/05/26 11:07:26 │ -│ 85% │ 63.722s │ 11.238s │ 06/05/26 11:07:27 │ -│ 90% │ 67.445s │ 7.487s │ 06/05/26 11:07:27 │ -│ 95% │ 71.165s │ 3.739s │ 06/05/26 11:07:27 │ -├──────────┴────────────────┴──────────────┴────────────────────┤ -│ Elapsed time: 74.88s │ -├───────────────────────────────────────────────────────────────┤ -│ Sampled data post-processing: Done │ -│ Elapsed time: 0.00s │ -├───────────────────────────────────────────────────────────────┤ -│ Summary │ -├───────────────────────────────────────────────────────────────┤ -│ Peak memory in use: 6098068408MB │ -├───────────────────────────────────────────────────────────────┤ -│ Total execution time: 74.92s │ -├───────────────────────────────────────────────────────────────┤ -│ End of computation │ -└───────────────────────────────────────────────────────────────┘ - Activating project at `~/INI_code/Julia II` -┌ Info: reading DICOM series -└ dicom_dir = "/Users/vm/INI_code/Ultrasound/DIRU_20240404_human_skull_phase_correction_1_2_(skull_Normal)/DICOM/PAT_0000/STD_0000/SER_0002/OBJ_0001" -┌ Info: cropping ROI -│ index_xyz = (170, 190, 400) -└ size_xyz = (705, 360, 450) -┌ Info: resampling x/y only -│ spacing_x_mm = 0.201171875 -│ spacing_y_mm = 0.201171875 -└ new_spacing_xy_mm = 0.2 -Saved PAM cluster outputs to /Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/07_aperture_ap20_dropout0p0_seed42/run diff --git a/runs/20260506_103348_pam_aperture_sweep/07_aperture_ap20_dropout0p0_seed42/run/activity_boundaries.png b/runs/20260506_103348_pam_aperture_sweep/07_aperture_ap20_dropout0p0_seed42/run/activity_boundaries.png deleted file mode 100644 index 3ac58b9..0000000 Binary files a/runs/20260506_103348_pam_aperture_sweep/07_aperture_ap20_dropout0p0_seed42/run/activity_boundaries.png and /dev/null differ diff --git a/runs/20260506_103348_pam_aperture_sweep/07_aperture_ap20_dropout0p0_seed42/run/overview.png b/runs/20260506_103348_pam_aperture_sweep/07_aperture_ap20_dropout0p0_seed42/run/overview.png deleted file mode 100644 index af485b5..0000000 Binary files a/runs/20260506_103348_pam_aperture_sweep/07_aperture_ap20_dropout0p0_seed42/run/overview.png and /dev/null differ diff --git a/runs/20260506_103348_pam_aperture_sweep/07_aperture_ap20_dropout0p0_seed42/run/summary.json b/runs/20260506_103348_pam_aperture_sweep/07_aperture_ap20_dropout0p0_seed42/run/summary.json deleted file mode 100644 index f966a6b..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/07_aperture_ap20_dropout0p0_seed42/run/summary.json +++ /dev/null @@ -1,2622 +0,0 @@ -{ - "reconstruction_bandwidth_hz": 500000, - "reconstruction_source": { - "mode": "simulation" - }, - "clusters": [ - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0060327725038961015, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -320.691671411612, - -481.03750711741804, - -641.383342823224 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.046321440287638786 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.005272737898667883, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -320.2235738069466, - -480.33536071041993, - -640.4471476138932 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04626569514702229 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0047312694272838695, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -317.8032279388882, - -476.70484190833236, - -635.6064558777764 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04572226666640846 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.004200866487451935, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -315.3467548948123, - -473.0201323422185, - -630.6935097896246 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04516619536230458 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0036565083902456493, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -312.9314759402867, - -469.39721391043014, - -625.8629518805734 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04461734980426383 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0031861889638953003, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -310.20409603501, - -465.3061440525151, - -620.40819207002 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04398719954353102 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.00256652716942303, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -308.0188633679521, - -462.0282950519281, - -616.0377267359042 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04348928418531734 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0017486279796490286, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -308.1907975338834, - -462.2861963008251, - -616.3815950677669 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04355435089147969 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.001125078113947233, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -309.8379271124607, - -464.75689066869114, - -619.6758542249214 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.043959799624315364 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0006028161597729909, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -312.11941106984887, - -468.17911660477336, - -624.2388221396977 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04451058220522799 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -2.401284689886321e-5, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -314.2460812377018, - -471.3691218565527, - -628.4921624754036 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.045020721921306135 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0004173119265126402, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -316.95746044523713, - -475.4361906678557, - -633.9149208904743 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04566686911546983 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0011125489663053104, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -318.6137903276162, - -477.9206854914243, - -637.2275806552324 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04605530266618959 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0017115638060362216, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -321.1407190707501, - -481.7110786061252, - -642.2814381415002 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04664759181635218 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0024823400105268, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -319.9942647843374, - -479.99139717650615, - -639.9885295686748 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.0463526618538167 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0031517892589508424, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -318.5017269206361, - -477.7525903809542, - -637.0034538412722 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04597133599677839 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.003865963263819151, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -316.9332073482421, - -475.3998110223631, - -633.8664146964842 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04556339958483016 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0043121080495665395, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -314.38452223900595, - -471.576783358509, - -628.7690444780119 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04492980070620905 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.004534541578681507, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -310.9262770322773, - -466.389415548416, - -621.8525540645546 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.044089545625208976 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.005237053357696269, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -309.35567932143914, - -464.03351898215874, - -618.7113586428783 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04366731027665283 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.005975114360697463, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -308.32565851717015, - -462.4884877757553, - -616.6513170343403 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.0433644115625137 - } - ], - "source_phase_mode": "random_phase_per_window", - "reconstruction_axial_step_m": 5.0e-5, - "detection_truth_mode": "centerline_tube", - "medium": { - "outer_row": 151, - "receiver_row": 1, - "hu_bone_thr": 200, - "slice_index": 250, - "inner_row": 186, - "aberrator": "skull", - "skull_to_transducer": 0.03, - "ct_path": "/Users/vm/INI_code/Ultrasound/DIRU_20240404_human_skull_phase_correction_1_2_(skull_Normal)/DICOM/PAT_0000/STD_0000/SER_0002/OBJ_0001" - }, - "analysis_mode": "detection", - "detection_threshold_ratio": 0.2, - "clean_threshold_ratio": 0.01, - "physical_source_count": 21, - "reconstruction_mode": "windowed", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/07_aperture_ap20_dropout0p0_seed42/run", - "window_info": { - "geometric": { - "effective_window_duration_s": 2.0e-5, - "used_window_ranges": [ - [ - 501, - 1500 - ], - [ - 1001, - 2000 - ], - [ - 1501, - 2500 - ], - [ - 2001, - 3000 - ], - [ - 2501, - 3500 - ], - [ - 3001, - 4000 - ], - [ - 3501, - 4500 - ], - [ - 4001, - 5000 - ], - [ - 4501, - 5500 - ], - [ - 5001, - 6000 - ], - [ - 5501, - 6500 - ], - [ - 6001, - 7000 - ], - [ - 6501, - 7500 - ], - [ - 7001, - 8000 - ], - [ - 7501, - 8500 - ], - [ - 8001, - 9000 - ], - [ - 8501, - 9500 - ], - [ - 9001, - 10000 - ], - [ - 9501, - 10500 - ], - [ - 10001, - 11000 - ], - [ - 10501, - 11500 - ], - [ - 11001, - 12000 - ], - [ - 11501, - 12500 - ], - [ - 12001, - 13000 - ], - [ - 12501, - 13500 - ], - [ - 13001, - 14000 - ], - [ - 13501, - 14500 - ], - [ - 14001, - 15000 - ], - [ - 14501, - 15500 - ], - [ - 15001, - 16000 - ], - [ - 15501, - 16500 - ], - [ - 16001, - 17000 - ], - [ - 16501, - 17500 - ], - [ - 17001, - 18000 - ], - [ - 17501, - 18500 - ], - [ - 18001, - 19000 - ], - [ - 18501, - 19500 - ], - [ - 19001, - 20000 - ], - [ - 19501, - 20500 - ], - [ - 20001, - 21000 - ], - [ - 20501, - 21500 - ], - [ - 21001, - 22000 - ], - [ - 21501, - 22500 - ], - [ - 22001, - 23000 - ], - [ - 22501, - 23500 - ], - [ - 23001, - 24000 - ], - [ - 23501, - 24500 - ], - [ - 24001, - 25000 - ] - ], - "used_window_count": 48, - "skipped_window_ranges": [ - [ - 1, - 1000 - ] - ], - "window_samples": 1000, - "effective_hop_s": 1.0e-5, - "energy_threshold": 167.74329213395228, - "skipped_window_count": 1, - "accumulation": "intensity", - "total_window_count": 49, - "hop_samples": 500 - }, - "hasa": { - "effective_window_duration_s": 2.0e-5, - "used_window_ranges": [ - [ - 501, - 1500 - ], - [ - 1001, - 2000 - ], - [ - 1501, - 2500 - ], - [ - 2001, - 3000 - ], - [ - 2501, - 3500 - ], - [ - 3001, - 4000 - ], - [ - 3501, - 4500 - ], - [ - 4001, - 5000 - ], - [ - 4501, - 5500 - ], - [ - 5001, - 6000 - ], - [ - 5501, - 6500 - ], - [ - 6001, - 7000 - ], - [ - 6501, - 7500 - ], - [ - 7001, - 8000 - ], - [ - 7501, - 8500 - ], - [ - 8001, - 9000 - ], - [ - 8501, - 9500 - ], - [ - 9001, - 10000 - ], - [ - 9501, - 10500 - ], - [ - 10001, - 11000 - ], - [ - 10501, - 11500 - ], - [ - 11001, - 12000 - ], - [ - 11501, - 12500 - ], - [ - 12001, - 13000 - ], - [ - 12501, - 13500 - ], - [ - 13001, - 14000 - ], - [ - 13501, - 14500 - ], - [ - 14001, - 15000 - ], - [ - 14501, - 15500 - ], - [ - 15001, - 16000 - ], - [ - 15501, - 16500 - ], - [ - 16001, - 17000 - ], - [ - 16501, - 17500 - ], - [ - 17001, - 18000 - ], - [ - 17501, - 18500 - ], - [ - 18001, - 19000 - ], - [ - 18501, - 19500 - ], - [ - 19001, - 20000 - ], - [ - 19501, - 20500 - ], - [ - 20001, - 21000 - ], - [ - 20501, - 21500 - ], - [ - 21001, - 22000 - ], - [ - 21501, - 22500 - ], - [ - 22001, - 23000 - ], - [ - 22501, - 23500 - ], - [ - 23001, - 24000 - ], - [ - 23501, - 24500 - ], - [ - 24001, - 25000 - ] - ], - "used_window_count": 48, - "skipped_window_ranges": [ - [ - 1, - 1000 - ] - ], - "window_samples": 1000, - "effective_hop_s": 1.0e-5, - "energy_threshold": 167.74329213395228, - "skipped_window_count": 1, - "accumulation": "intensity", - "total_window_count": 49, - "hop_samples": 500 - } - }, - "window_config": { - "window_duration_s": 1.9999999999999998e-5, - "enabled": true, - "hop_s": 9.999999999999999e-6, - "min_energy_ratio": 0.001, - "taper": "hann", - "accumulation": "intensity" - }, - "activity_boundary_figure": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/07_aperture_ap20_dropout0p0_seed42/run/activity_boundaries.png", - "reference_sound_speed_m_per_s": 1551.0647917287392, - "emission_event_count": 21, - "hasa": { - "truth_components": 1, - "true_negative_pixels": 180312, - "energy_outside_predicted_mask": 3.5615877561749496e9, - "centroid_depth_mm": 45.376793780724796, - "energy_total": 1.3167779086861366e10, - "false_negative_area_mm2": 1.8, - "centroid_error_mm": 0.6298582959424722, - "target_axial_spread_mm": 1.302899781789997, - "prediction_components": 1, - "false_negative_pixels": 45, - "psf_target_ssim_like": 0.27455231614681735, - "matched_prediction_components": 1, - "recall": 0.9473684210526315, - "f1": 0.06403668274171871, - "energy_fraction_outside_predicted_mask": 0.27047748391592125, - "psf_target_mode": "provided_mask", - "energy_outside_mask": 1.2722865814730785e10, - "energy_fraction_inside_predicted_mask": 0.7295225160840787, - "axial_spread_mm": 22.075315222521233, - "false_positive_area_mm2": 945.32, - "missed_truth_components": 0, - "psf_target_normalized_l2_error": 0.9706298799608375, - "psf_target_correlation": 0.24714886487972174, - "true_positive_pixels": 810, - "target_centroid_depth_mm": 44.999999999999936, - "energy_fraction_outside_mask": 0.966211973241979, - "num_truth_sources": 21, - "recovered_truth_components": 1, - "target_centroid_lateral_mm": 2.370764649070736e-17, - "psf_axial_fwhm_mm": 0.75, - "energy_inside_mask": 4.4491327213058126e8, - "precision": 0.03313832180992513, - "dice": 0.06403668274171871, - "jaccard": 0.03307742567788304, - "centroid_lateral_mm": -0.5047255885871262, - "peak_mm": [ - 36.6, - -1.8000000000000003 - ], - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "predicted_area_mm2": 977.72, - "overlap_area_mm2": 32.4, - "max_intensity": 1.237121556509556e6, - "threshold_ratio": 0.2, - "false_positive_pixels": 23633, - "threshold_db": -6.9897000433601875, - "truth_mask_mode": "provided", - "energy_inside_predicted_mask": 9.606191330686417e9, - "truth_radius_mm": 1, - "energy_fraction_inside_mask": 0.033788026758021006, - "psf_target_integral": 855, - "truth_area_mm2": 34.2, - "spurious_prediction_components": 0, - "target_lateral_spread_mm": 3.8376998229906123, - "psf_lateral_fwhm_mm": 1.125, - "lateral_spread_mm": 7.969343602688401, - "peak_intensity": 1.237121556509556e6 - }, - "config": { - "axial_dim": 0.08, - "dt": 2.0e-8, - "bottom_margin": 0.01, - "rho0": 1000, - "success_tolerance": 0.0015, - "dx": 0.0002, - "receiver_aperture": 0.02, - "dz": 0.0002, - "zero_pad_factor": 4, - "transverse_dim": 0.1024, - "t_max": 0.0005, - "c0": 1500, - "peak_suppression_radius": 0.008 - }, - "peak_method": "argmax", - "clean_loop_gain": 0.1, - "geometric": { - "truth_components": 1, - "true_negative_pixels": 175887, - "energy_outside_predicted_mask": 1.2836766049417505e9, - "centroid_depth_mm": 39.97361605107475, - "energy_total": 8.891765403665781e9, - "false_negative_area_mm2": 2.68, - "centroid_error_mm": 5.033588534889233, - "target_axial_spread_mm": 1.302899781789997, - "prediction_components": 1, - "false_negative_pixels": 67, - "psf_target_ssim_like": 0.26288035537735227, - "matched_prediction_components": 1, - "recall": 0.9216374269005848, - "f1": 0.05306218645836841, - "energy_fraction_outside_predicted_mask": 0.14436689978488784, - "psf_target_mode": "provided_mask", - "energy_outside_mask": 8.641760417070343e9, - "energy_fraction_inside_predicted_mask": 0.8556331002151122, - "axial_spread_mm": 23.029921810386814, - "false_positive_area_mm2": 1122.32, - "missed_truth_components": 0, - "psf_target_normalized_l2_error": 0.9794888092047312, - "psf_target_correlation": 0.18961527742847792, - "true_positive_pixels": 788, - "target_centroid_depth_mm": 44.999999999999936, - "energy_fraction_outside_mask": 0.9718835377176764, - "num_truth_sources": 21, - "recovered_truth_components": 1, - "target_centroid_lateral_mm": 2.370764649070736e-17, - "psf_axial_fwhm_mm": 0.75, - "energy_inside_mask": 2.5000498659543887e8, - "precision": 0.02731747902655481, - "dice": 0.0530621864583684, - "jaccard": 0.027254176322069657, - "centroid_lateral_mm": 0.26921726645143695, - "peak_mm": [ - 24.800000000000004, - -1.0 - ], - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "predicted_area_mm2": 1153.84, - "overlap_area_mm2": 31.52, - "max_intensity": 551996.4042871256, - "threshold_ratio": 0.2, - "false_positive_pixels": 28058, - "threshold_db": -6.9897000433601875, - "truth_mask_mode": "provided", - "energy_inside_predicted_mask": 7.6080887987240305e9, - "truth_radius_mm": 1, - "energy_fraction_inside_mask": 0.028116462282323607, - "psf_target_integral": 855, - "truth_area_mm2": 34.2, - "spurious_prediction_components": 0, - "target_lateral_spread_mm": 3.8376998229906123, - "psf_lateral_fwhm_mm": 1.125, - "lateral_spread_mm": 6.661801330410844, - "peak_intensity": 551996.4042871256 - }, - "clean_max_iter": 500, - "source_variability": { - "amplitude_distribution": "fixed", - "frequency_jitter_percent": 1, - "amplitude_sigma": 0, - "dropout_probability": 0 - }, - "emission_meta": { - "emission_event_count": 21, - "random_seed": 42, - "harmonics": [ - 2, - 3, - 4 - ], - "phase_jitter_rad": 0.2, - "n_bubbles_per_cluster": [ - 10 - ], - "cluster_model": "vascular", - "phase_mode": "geometric", - "n_emission_sources": 21, - "cavitation_model": "harmonic_cos", - "gate_duration_s": 4.9999999999999996e-5, - "delays_s": [ - 0 - ], - "activity_model": { - "emission_event_count": 21, - "activity_mode": "static", - "phase_jitter_rad": 0, - "active_probability": 1, - "physical_source_count": 21, - "hop_s": null, - "amplitude_jitter": 0, - "frame_duration_s": null - }, - "transducer_m": [ - -0.03, - 0.0 - ], - "fundamental_hz": 500000, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "vascular": { - "branch_levels": 2, - "source_spacing_m": 0.0008, - "bundle_count": 3, - "centerlines_m": [ - [ - [ - 0.0465, - -0.006 - ], - [ - 0.046426584774442725, - -0.0056 - ], - [ - 0.04621352549156242, - -0.0052 - ], - [ - 0.045881677878438706, - -0.0048 - ], - [ - 0.04546352549156242, - -0.0044 - ], - [ - 0.045, - -0.004 - ], - [ - 0.044536474508437576, - -0.0036 - ], - [ - 0.04411832212156129, - -0.0032 - ], - [ - 0.043786474508437576, - -0.0028 - ], - [ - 0.043573415225557265, - -0.0024 - ], - [ - 0.0435, - -0.002 - ], - [ - 0.04357341522555727, - -0.0016 - ], - [ - 0.043786474508437576, - -0.0012 - ], - [ - 0.04411832212156129, - -0.0008 - ], - [ - 0.044536474508437576, - -0.0004 - ], - [ - 0.045, - 0.0 - ], - [ - 0.04546352549156242, - 0.0004 - ], - [ - 0.045881677878438706, - 0.0008 - ], - [ - 0.04621352549156242, - 0.0012 - ], - [ - 0.046426584774442725, - 0.0016 - ], - [ - 0.0465, - 0.002 - ], - [ - 0.04642658477444273, - 0.0024 - ], - [ - 0.04621352549156242, - 0.0028 - ], - [ - 0.045881677878438706, - 0.0032 - ], - [ - 0.04546352549156242, - 0.0036 - ], - [ - 0.045, - 0.004 - ], - [ - 0.044536474508437576, - 0.0044 - ], - [ - 0.04411832212156129, - 0.0048 - ], - [ - 0.043786474508437576, - 0.0052 - ], - [ - 0.04357341522555727, - 0.0056 - ], - [ - 0.0435, - 0.006 - ] - ] - ], - "min_separation_m": 0.0003, - "branch_angle_rad": 0.5235987755982988, - "position_jitter_m": 0.00015, - "squiggle_amplitude_m": 0.0015, - "anchors": [ - { - "segments_m": [ - ], - "anchor_m": [ - 0.045, - 0.0 - ], - "source_count": 21, - "centerlines_m": [ - [ - [ - 0.0465, - -0.006 - ], - [ - 0.046426584774442725, - -0.0056 - ], - [ - 0.04621352549156242, - -0.0052 - ], - [ - 0.045881677878438706, - -0.0048 - ], - [ - 0.04546352549156242, - -0.0044 - ], - [ - 0.045, - -0.004 - ], - [ - 0.044536474508437576, - -0.0036 - ], - [ - 0.04411832212156129, - -0.0032 - ], - [ - 0.043786474508437576, - -0.0028 - ], - [ - 0.043573415225557265, - -0.0024 - ], - [ - 0.0435, - -0.002 - ], - [ - 0.04357341522555727, - -0.0016 - ], - [ - 0.043786474508437576, - -0.0012 - ], - [ - 0.04411832212156129, - -0.0008 - ], - [ - 0.044536474508437576, - -0.0004 - ], - [ - 0.045, - 0.0 - ], - [ - 0.04546352549156242, - 0.0004 - ], - [ - 0.045881677878438706, - 0.0008 - ], - [ - 0.04621352549156242, - 0.0012 - ], - [ - 0.046426584774442725, - 0.0016 - ], - [ - 0.0465, - 0.002 - ], - [ - 0.04642658477444273, - 0.0024 - ], - [ - 0.04621352549156242, - 0.0028 - ], - [ - 0.045881677878438706, - 0.0032 - ], - [ - 0.04546352549156242, - 0.0036 - ], - [ - 0.045, - 0.004 - ], - [ - 0.044536474508437576, - 0.0044 - ], - [ - 0.04411832212156129, - 0.0048 - ], - [ - 0.043786474508437576, - 0.0052 - ], - [ - 0.04357341522555727, - 0.0056 - ], - [ - 0.0435, - 0.006 - ] - ] - ] - } - ], - "branch_scale": 0.65, - "topology": "squiggle", - "max_sources_per_anchor": 0, - "truth_radius_m": 0.001, - "squiggle_wavelength_m": 0.008, - "length_m": 0.012, - "squiggle_slope": 0, - "bundle_spacing_m": 0.002 - }, - "n_anchor_clusters": 1, - "physical_source_count": 21, - "anchor_clusters_m": [ - [ - 0.045, - 0.0 - ] - ] - }, - "n_realizations": 1, - "activity_model": { - "emission_event_count": 21, - "activity_mode": "static", - "phase_jitter_rad": 0, - "active_probability": 1, - "physical_source_count": 21, - "hop_s": null, - "amplitude_jitter": 0, - "frame_duration_s": null - }, - "detection_truth_radius_m": 0.001, - "boundary_threshold_ratios": [ - 0.6, - 0.65, - 0.7 - ], - "simulation": { - "receiver_row": 1, - "source_indices": [ - [ - 233, - 227 - ], - [ - 232, - 231 - ], - [ - 230, - 233 - ], - [ - 227, - 236 - ], - [ - 224, - 239 - ], - [ - 221, - 241 - ], - [ - 218, - 244 - ], - [ - 219, - 248 - ], - [ - 221, - 251 - ], - [ - 224, - 254 - ], - [ - 226, - 257 - ], - [ - 229, - 259 - ], - [ - 231, - 263 - ], - [ - 234, - 266 - ], - [ - 233, - 269 - ], - [ - 231, - 273 - ], - [ - 229, - 276 - ], - [ - 226, - 279 - ], - [ - 221, - 280 - ], - [ - 219, - 283 - ], - [ - 218, - 287 - ] - ], - "receiver_cols": [ - 207, - 306 - ] - }, - "reconstruction_frequencies_hz": [ - 1000000, - 1500000, - 2000000 - ], - "activity_boundary_metrics": { - "geometric": [ - { - "target_centroid_lateral_mm": 2.370764649070736e-17, - "axial_spread_mm": 23.029921810386814, - "peak_intensity": 551996.4042871256, - "dice": 0.12082853855005754, - "false_positive_area_mm2": 227.08, - "energy_inside_predicted_mask": 2.4466858513449745e9, - "overlap_area_mm2": 16.8, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.72483688668436, - "centroid_lateral_mm": 0.26921726645143695, - "f1": 0.12082853855005753, - "threshold_ratio": 0.6, - "false_positive_pixels": 5677, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 1.125, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.999999999999936, - "true_negative_pixels": 198268, - "energy_outside_predicted_mask": 6.4450795523208065e9, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 8.891765403665781e9, - "psf_target_integral": 855, - "spurious_prediction_components": 0, - "truth_components": 1, - "target_axial_spread_mm": 1.302899781789997, - "jaccard": 0.06429883649724434, - "psf_target_correlation": 0.18961527742847792, - "psf_target_ssim_like": 0.26288035537735227, - "precision": 0.06888633754305395, - "peak_mm": [ - 24.800000000000004, - -1.0 - ], - "energy_fraction_inside_predicted_mask": 0.27516311331564, - "recovered_truth_components": 1, - "lateral_spread_mm": 6.661801330410844, - "true_positive_pixels": 420, - "centroid_error_mm": 5.033588534889233, - "recall": 0.49122807017543857, - "matched_prediction_components": 1, - "prediction_components": 1, - "false_negative_pixels": 435, - "energy_fraction_inside_mask": 0.028116462282323607, - "predicted_area_mm2": 243.88, - "energy_fraction_outside_mask": 0.9718835377176764, - "max_intensity": 551996.4042871256, - "energy_inside_mask": 2.5000498659543887e8, - "centroid_depth_mm": 39.97361605107475, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8376998229906123, - "psf_target_normalized_l2_error": 0.9794888092047312, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 17.400000000000002, - "energy_outside_mask": 8.641760417070343e9, - "num_truth_sources": 21, - "threshold_db": -2.218487496163564 - }, - { - "target_centroid_lateral_mm": 2.370764649070736e-17, - "axial_spread_mm": 23.029921810386814, - "peak_intensity": 551996.4042871256, - "dice": 0.06998087954110899, - "false_positive_area_mm2": 167.68, - "energy_inside_predicted_mask": 1.8542765514697187e9, - "overlap_area_mm2": 7.32, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.7914613727094889, - "centroid_lateral_mm": 0.26921726645143695, - "f1": 0.06998087954110899, - "threshold_ratio": 0.65, - "false_positive_pixels": 4192, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 1.125, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.999999999999936, - "true_negative_pixels": 199753, - "energy_outside_predicted_mask": 7.037488852196062e9, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 8.891765403665781e9, - "psf_target_integral": 855, - "spurious_prediction_components": 1, - "truth_components": 1, - "target_axial_spread_mm": 1.302899781789997, - "jaccard": 0.03625916385971865, - "psf_target_correlation": 0.18961527742847792, - "psf_target_ssim_like": 0.26288035537735227, - "precision": 0.04182857142857143, - "peak_mm": [ - 24.800000000000004, - -1.0 - ], - "energy_fraction_inside_predicted_mask": 0.20853862729051104, - "recovered_truth_components": 1, - "lateral_spread_mm": 6.661801330410844, - "true_positive_pixels": 183, - "centroid_error_mm": 5.033588534889233, - "recall": 0.21403508771929824, - "matched_prediction_components": 1, - "prediction_components": 2, - "false_negative_pixels": 672, - "energy_fraction_inside_mask": 0.028116462282323607, - "predicted_area_mm2": 175, - "energy_fraction_outside_mask": 0.9718835377176764, - "max_intensity": 551996.4042871256, - "energy_inside_mask": 2.5000498659543887e8, - "centroid_depth_mm": 39.97361605107475, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8376998229906123, - "psf_target_normalized_l2_error": 0.9794888092047312, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 26.88, - "energy_outside_mask": 8.641760417070343e9, - "num_truth_sources": 21, - "threshold_db": -1.8708664335714442 - }, - { - "target_centroid_lateral_mm": 2.370764649070736e-17, - "axial_spread_mm": 23.029921810386814, - "peak_intensity": 551996.4042871256, - "dice": 0.0033849129593810446, - "false_positive_area_mm2": 130.96, - "energy_inside_predicted_mask": 1.4475142956344128e9, - "overlap_area_mm2": 0.28, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.8372073227395709, - "centroid_lateral_mm": 0.26921726645143695, - "f1": 0.003384912959381045, - "threshold_ratio": 0.7, - "false_positive_pixels": 3274, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 1.125, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.999999999999936, - "true_negative_pixels": 200671, - "energy_outside_predicted_mask": 7.444251108031368e9, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 8.891765403665781e9, - "psf_target_integral": 855, - "spurious_prediction_components": 0, - "truth_components": 1, - "target_axial_spread_mm": 1.302899781789997, - "jaccard": 0.0016953257447323807, - "psf_target_correlation": 0.18961527742847792, - "psf_target_ssim_like": 0.26288035537735227, - "precision": 0.0021334958854007926, - "peak_mm": [ - 24.800000000000004, - -1.0 - ], - "energy_fraction_inside_predicted_mask": 0.16279267726042912, - "recovered_truth_components": 1, - "lateral_spread_mm": 6.661801330410844, - "true_positive_pixels": 7, - "centroid_error_mm": 5.033588534889233, - "recall": 0.008187134502923977, - "matched_prediction_components": 1, - "prediction_components": 1, - "false_negative_pixels": 848, - "energy_fraction_inside_mask": 0.028116462282323607, - "predicted_area_mm2": 131.24, - "energy_fraction_outside_mask": 0.9718835377176764, - "max_intensity": 551996.4042871256, - "energy_inside_mask": 2.5000498659543887e8, - "centroid_depth_mm": 39.97361605107475, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8376998229906123, - "psf_target_normalized_l2_error": 0.9794888092047312, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 33.92, - "energy_outside_mask": 8.641760417070343e9, - "num_truth_sources": 21, - "threshold_db": -1.5490195998574319 - } - ], - "hasa": [ - { - "target_centroid_lateral_mm": 2.370764649070736e-17, - "axial_spread_mm": 22.075315222521233, - "peak_intensity": 1.237121556509556e6, - "dice": 0.018018018018018018, - "false_positive_area_mm2": 23, - "energy_inside_predicted_mask": 5.1606588481036896e8, - "overlap_area_mm2": 0.52, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.9608084338743734, - "centroid_lateral_mm": -0.5047255885871262, - "f1": 0.018018018018018018, - "threshold_ratio": 0.6, - "false_positive_pixels": 575, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 1.125, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.999999999999936, - "true_negative_pixels": 203370, - "energy_outside_predicted_mask": 1.2651713202050997e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 1.3167779086861366e10, - "psf_target_integral": 855, - "spurious_prediction_components": 1, - "truth_components": 1, - "target_axial_spread_mm": 1.302899781789997, - "jaccard": 0.00909090909090909, - "psf_target_correlation": 0.24714886487972174, - "psf_target_ssim_like": 0.27455231614681735, - "precision": 0.022108843537414966, - "peak_mm": [ - 36.6, - -1.8000000000000003 - ], - "energy_fraction_inside_predicted_mask": 0.0391915661256265, - "recovered_truth_components": 1, - "lateral_spread_mm": 7.969343602688401, - "true_positive_pixels": 13, - "centroid_error_mm": 0.6298582959424722, - "recall": 0.0152046783625731, - "matched_prediction_components": 1, - "prediction_components": 2, - "false_negative_pixels": 842, - "energy_fraction_inside_mask": 0.033788026758021006, - "predicted_area_mm2": 23.52, - "energy_fraction_outside_mask": 0.966211973241979, - "max_intensity": 1.237121556509556e6, - "energy_inside_mask": 4.4491327213058126e8, - "centroid_depth_mm": 45.376793780724796, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8376998229906123, - "psf_target_normalized_l2_error": 0.9706298799608375, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 33.68, - "energy_outside_mask": 1.2722865814730785e10, - "num_truth_sources": 21, - "threshold_db": -2.218487496163564 - }, - { - "target_centroid_lateral_mm": 2.370764649070736e-17, - "axial_spread_mm": 22.075315222521233, - "peak_intensity": 1.237121556509556e6, - "dice": 0, - "false_positive_area_mm2": 14.92, - "energy_inside_predicted_mask": 3.509121032450687e8, - "overlap_area_mm2": 0, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.9733506993905141, - "centroid_lateral_mm": -0.5047255885871262, - "f1": 0, - "threshold_ratio": 0.65, - "false_positive_pixels": 373, - "missed_truth_components": 1, - "psf_lateral_fwhm_mm": 1.125, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.999999999999936, - "true_negative_pixels": 203572, - "energy_outside_predicted_mask": 1.2816866983616297e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 1.3167779086861366e10, - "psf_target_integral": 855, - "spurious_prediction_components": 2, - "truth_components": 1, - "target_axial_spread_mm": 1.302899781789997, - "jaccard": 0, - "psf_target_correlation": 0.24714886487972174, - "psf_target_ssim_like": 0.27455231614681735, - "precision": 0, - "peak_mm": [ - 36.6, - -1.8000000000000003 - ], - "energy_fraction_inside_predicted_mask": 0.026649300609485782, - "recovered_truth_components": 0, - "lateral_spread_mm": 7.969343602688401, - "true_positive_pixels": 0, - "centroid_error_mm": 0.6298582959424722, - "recall": 0, - "matched_prediction_components": 0, - "prediction_components": 2, - "false_negative_pixels": 855, - "energy_fraction_inside_mask": 0.033788026758021006, - "predicted_area_mm2": 14.92, - "energy_fraction_outside_mask": 0.966211973241979, - "max_intensity": 1.237121556509556e6, - "energy_inside_mask": 4.4491327213058126e8, - "centroid_depth_mm": 45.376793780724796, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8376998229906123, - "psf_target_normalized_l2_error": 0.9706298799608375, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 34.2, - "energy_outside_mask": 1.2722865814730785e10, - "num_truth_sources": 21, - "threshold_db": -1.8708664335714442 - }, - { - "target_centroid_lateral_mm": 2.370764649070736e-17, - "axial_spread_mm": 22.075315222521233, - "peak_intensity": 1.237121556509556e6, - "dice": 0, - "false_positive_area_mm2": 9.24, - "energy_inside_predicted_mask": 2.3259171973389775e8, - "overlap_area_mm2": 0, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.9823362984600816, - "centroid_lateral_mm": -0.5047255885871262, - "f1": 0, - "threshold_ratio": 0.7, - "false_positive_pixels": 231, - "missed_truth_components": 1, - "psf_lateral_fwhm_mm": 1.125, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.999999999999936, - "true_negative_pixels": 203714, - "energy_outside_predicted_mask": 1.2935187367127468e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 1.3167779086861366e10, - "psf_target_integral": 855, - "spurious_prediction_components": 2, - "truth_components": 1, - "target_axial_spread_mm": 1.302899781789997, - "jaccard": 0, - "psf_target_correlation": 0.24714886487972174, - "psf_target_ssim_like": 0.27455231614681735, - "precision": 0, - "peak_mm": [ - 36.6, - -1.8000000000000003 - ], - "energy_fraction_inside_predicted_mask": 0.01766370153991835, - "recovered_truth_components": 0, - "lateral_spread_mm": 7.969343602688401, - "true_positive_pixels": 0, - "centroid_error_mm": 0.6298582959424722, - "recall": 0, - "matched_prediction_components": 0, - "prediction_components": 2, - "false_negative_pixels": 855, - "energy_fraction_inside_mask": 0.033788026758021006, - "predicted_area_mm2": 9.24, - "energy_fraction_outside_mask": 0.966211973241979, - "max_intensity": 1.237121556509556e6, - "energy_inside_mask": 4.4491327213058126e8, - "centroid_depth_mm": 45.376793780724796, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8376998229906123, - "psf_target_normalized_l2_error": 0.9706298799608375, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 34.2, - "energy_outside_mask": 1.2722865814730785e10, - "num_truth_sources": 21, - "threshold_db": -1.5490195998574319 - } - ], - "threshold_ratios": [ - 0.6, - 0.65, - 0.7 - ] - } -} \ No newline at end of file diff --git a/runs/20260506_103348_pam_aperture_sweep/07_aperture_ap20_dropout0p0_seed42/status.json b/runs/20260506_103348_pam_aperture_sweep/07_aperture_ap20_dropout0p0_seed42/status.json deleted file mode 100644 index 1f392c5..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/07_aperture_ap20_dropout0p0_seed42/status.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "best_hasa_f1": 0.018018018018018018, - "best_hasa_recall": 0.0152046783625731, - "hasa_psf_corr": 0.24714886487972174, - "best_geo_threshold": 0.6, - "hasa_centroid_error_mm": 0.6298582959424722, - "best_hasa_jaccard": 0.00909090909090909, - "summary_found": true, - "kind": "simulation", - "best_hasa_precision": 0.022108843537414966, - "status": "success", - "elapsed_min": 4.09, - "id": "aperture_ap20_dropout0p0_seed42", - "best_geo_f1": 0.12082853855005753, - "cmd": "`/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/07_aperture_ap20_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=20 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle`", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/07_aperture_ap20_dropout0p0_seed42/run", - "hasa_psf_l2": 0.9706298799608375, - "best_hasa_threshold": 0.6 -} diff --git a/runs/20260506_103348_pam_aperture_sweep/08_aperture_ap50_dropout0p25_seed42/run.log b/runs/20260506_103348_pam_aperture_sweep/08_aperture_ap50_dropout0p25_seed42/run.log deleted file mode 100644 index 7177e02..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/08_aperture_ap50_dropout0p25_seed42/run.log +++ /dev/null @@ -1,81 +0,0 @@ -# 2026-05-06T11:09:55.352 -# `/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.25 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/08_aperture_ap50_dropout0p25_seed42/run' --random-seed=42 --receiver-aperture-mm=50 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle` -WARNING:root:Highest prime factors in each dimension are [11 23] -WARNING:root:Use dimension sizes with lower prime factors to improve speed -┌───────────────────────────────────────────────────────────────┐ -│ kspaceFirstOrder-OMP v1.3 │ -├───────────────────────────────────────────────────────────────┤ -│ Reading simulation configuration: Done │ -│ Number of CPU threads: 12 │ -│ Processor name: │ -├───────────────────────────────────────────────────────────────┤ -│ Simulation details │ -├───────────────────────────────────────────────────────────────┤ -│ Domain dimensions: 440 x 552 │ -│ Medium type: 2D │ -│ Simulation time steps: 25000 │ -├───────────────────────────────────────────────────────────────┤ -│ Initialization │ -├───────────────────────────────────────────────────────────────┤ -│ Memory allocation: Done │ -│ Data loading: Done │ -│ Elapsed time: 0.01s │ -├───────────────────────────────────────────────────────────────┤ -│ FFT plans creation: Done │ -│ Pre-processing phase: Done │ -│ Elapsed time: 0.02s │ -├───────────────────────────────────────────────────────────────┤ -│ Computational resources │ -├───────────────────────────────────────────────────────────────┤ -│ Current host memory in use: 36020769710MB │ -│ Expected output file size: 23MB │ -├───────────────────────────────────────────────────────────────┤ -│ Simulation │ -├──────────┬────────────────┬──────────────┬────────────────────┤ -│ Progress │ Elapsed time │ Time to go │ Est. finish time │ -├──────────┼────────────────┼──────────────┼────────────────────┤ -│ 0% │ 0.009s │ 116.065s │ 06/05/26 11:12:13 │ -│ 5% │ 3.926s │ 74.472s │ 06/05/26 11:11:35 │ -│ 10% │ 7.628s │ 68.590s │ 06/05/26 11:11:33 │ -│ 15% │ 11.497s │ 65.111s │ 06/05/26 11:11:34 │ -│ 20% │ 15.279s │ 61.086s │ 06/05/26 11:11:34 │ -│ 25% │ 19.027s │ 57.058s │ 06/05/26 11:11:33 │ -│ 30% │ 22.835s │ 53.262s │ 06/05/26 11:11:33 │ -│ 35% │ 26.488s │ 49.175s │ 06/05/26 11:11:33 │ -│ 40% │ 30.133s │ 45.184s │ 06/05/26 11:11:32 │ -│ 45% │ 34.007s │ 41.551s │ 06/05/26 11:11:32 │ -│ 50% │ 37.727s │ 37.715s │ 06/05/26 11:11:32 │ -│ 55% │ 41.676s │ 34.087s │ 06/05/26 11:11:33 │ -│ 60% │ 45.470s │ 30.304s │ 06/05/26 11:11:33 │ -│ 65% │ 49.323s │ 26.549s │ 06/05/26 11:11:33 │ -│ 70% │ 53.000s │ 22.705s │ 06/05/26 11:11:32 │ -│ 75% │ 56.615s │ 18.864s │ 06/05/26 11:11:32 │ -│ 80% │ 60.251s │ 15.055s │ 06/05/26 11:11:33 │ -│ 85% │ 63.878s │ 11.265s │ 06/05/26 11:11:32 │ -│ 90% │ 67.697s │ 7.515s │ 06/05/26 11:11:32 │ -│ 95% │ 71.330s │ 3.748s │ 06/05/26 11:11:32 │ -├──────────┴────────────────┴──────────────┴────────────────────┤ -│ Elapsed time: 75.08s │ -├───────────────────────────────────────────────────────────────┤ -│ Sampled data post-processing: Done │ -│ Elapsed time: 0.00s │ -├───────────────────────────────────────────────────────────────┤ -│ Summary │ -├───────────────────────────────────────────────────────────────┤ -│ Peak memory in use: 6097904568MB │ -├───────────────────────────────────────────────────────────────┤ -│ Total execution time: 75.12s │ -├───────────────────────────────────────────────────────────────┤ -│ End of computation │ -└───────────────────────────────────────────────────────────────┘ - Activating project at `~/INI_code/Julia II` -┌ Info: reading DICOM series -└ dicom_dir = "/Users/vm/INI_code/Ultrasound/DIRU_20240404_human_skull_phase_correction_1_2_(skull_Normal)/DICOM/PAT_0000/STD_0000/SER_0002/OBJ_0001" -┌ Info: cropping ROI -│ index_xyz = (170, 190, 400) -└ size_xyz = (705, 360, 450) -┌ Info: resampling x/y only -│ spacing_x_mm = 0.201171875 -│ spacing_y_mm = 0.201171875 -└ new_spacing_xy_mm = 0.2 -Saved PAM cluster outputs to /Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/08_aperture_ap50_dropout0p25_seed42/run diff --git a/runs/20260506_103348_pam_aperture_sweep/08_aperture_ap50_dropout0p25_seed42/run/activity_boundaries.png b/runs/20260506_103348_pam_aperture_sweep/08_aperture_ap50_dropout0p25_seed42/run/activity_boundaries.png deleted file mode 100644 index c93ace1..0000000 Binary files a/runs/20260506_103348_pam_aperture_sweep/08_aperture_ap50_dropout0p25_seed42/run/activity_boundaries.png and /dev/null differ diff --git a/runs/20260506_103348_pam_aperture_sweep/08_aperture_ap50_dropout0p25_seed42/run/overview.png b/runs/20260506_103348_pam_aperture_sweep/08_aperture_ap50_dropout0p25_seed42/run/overview.png deleted file mode 100644 index 7dbeddc..0000000 Binary files a/runs/20260506_103348_pam_aperture_sweep/08_aperture_ap50_dropout0p25_seed42/run/overview.png and /dev/null differ diff --git a/runs/20260506_103348_pam_aperture_sweep/08_aperture_ap50_dropout0p25_seed42/run/summary.json b/runs/20260506_103348_pam_aperture_sweep/08_aperture_ap50_dropout0p25_seed42/run/summary.json deleted file mode 100644 index 1cf5720..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/08_aperture_ap50_dropout0p25_seed42/run/summary.json +++ /dev/null @@ -1,2622 +0,0 @@ -{ - "reconstruction_bandwidth_hz": 500000, - "reconstruction_source": { - "mode": "simulation" - }, - "clusters": [ - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0060327725038961015, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -320.691671411612, - -481.03750711741804, - -641.383342823224 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.046321440287638786 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.005272737898667883, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -320.2235738069466, - -480.33536071041993, - -640.4471476138932 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04626569514702229 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0047312694272838695, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -317.8032279388882, - -476.70484190833236, - -635.6064558777764 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04572226666640846 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.004200866487451935, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -315.3467548948123, - -473.0201323422185, - -630.6935097896246 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04516619536230458 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0036565083902456493, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -312.9314759402867, - -469.39721391043014, - -625.8629518805734 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04461734980426383 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0031861889638953003, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -310.20409603501, - -465.3061440525151, - -620.40819207002 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04398719954353102 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.00256652716942303, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -308.0188633679521, - -462.0282950519281, - -616.0377267359042 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04348928418531734 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0017486279796490286, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -308.1907975338834, - -462.2861963008251, - -616.3815950677669 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04355435089147969 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.001125078113947233, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -309.8379271124607, - -464.75689066869114, - -619.6758542249214 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.043959799624315364 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -0.0006028161597729909, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -312.11941106984887, - -468.17911660477336, - -624.2388221396977 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04451058220522799 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": -2.401284689886321e-5, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -314.2460812377018, - -471.3691218565527, - -628.4921624754036 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.045020721921306135 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0004173119265126402, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -316.95746044523713, - -475.4361906678557, - -633.9149208904743 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04566686911546983 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0011125489663053104, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -318.6137903276162, - -477.9206854914243, - -637.2275806552324 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04605530266618959 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0017115638060362216, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -321.1407190707501, - -481.7110786061252, - -642.2814381415002 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04664759181635218 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0024823400105268, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -319.9942647843374, - -479.99139717650615, - -639.9885295686748 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.0463526618538167 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0031517892589508424, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -318.5017269206361, - -477.7525903809542, - -637.0034538412722 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04597133599677839 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.003865963263819151, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -316.9332073482421, - -475.3998110223631, - -633.8664146964842 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04556339958483016 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.0043121080495665395, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -314.38452223900595, - -471.576783358509, - -628.7690444780119 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04492980070620905 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.004534541578681507, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -310.9262770322773, - -466.389415548416, - -621.8525540645546 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.044089545625208976 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.005237053357696269, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -309.35567932143914, - -464.03351898215874, - -618.7113586428783 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.04366731027665283 - }, - { - "harmonics": [ - 2, - 3, - 4 - ], - "lateral_m": 0.005975114360697463, - "amplitude_pa": 1, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "n_bubbles": 10, - "harmonic_phases_rad": [ - -308.32565851717015, - -462.4884877757553, - -616.6513170343403 - ], - "gate_duration_s": 4.9999999999999996e-5, - "cavitation_model": "harmonic_cos", - "delay_s": 0, - "fundamental_hz": 500000, - "depth_m": 0.0433644115625137 - } - ], - "source_phase_mode": "random_phase_per_window", - "reconstruction_axial_step_m": 5.0e-5, - "detection_truth_mode": "centerline_tube", - "medium": { - "outer_row": 151, - "receiver_row": 1, - "hu_bone_thr": 200, - "slice_index": 250, - "inner_row": 186, - "aberrator": "skull", - "skull_to_transducer": 0.03, - "ct_path": "/Users/vm/INI_code/Ultrasound/DIRU_20240404_human_skull_phase_correction_1_2_(skull_Normal)/DICOM/PAT_0000/STD_0000/SER_0002/OBJ_0001" - }, - "analysis_mode": "detection", - "detection_threshold_ratio": 0.2, - "clean_threshold_ratio": 0.01, - "physical_source_count": 21, - "reconstruction_mode": "windowed", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/08_aperture_ap50_dropout0p25_seed42/run", - "window_info": { - "geometric": { - "effective_window_duration_s": 2.0e-5, - "used_window_ranges": [ - [ - 1001, - 2000 - ], - [ - 1501, - 2500 - ], - [ - 2001, - 3000 - ], - [ - 2501, - 3500 - ], - [ - 3001, - 4000 - ], - [ - 3501, - 4500 - ], - [ - 4001, - 5000 - ], - [ - 4501, - 5500 - ], - [ - 5001, - 6000 - ], - [ - 5501, - 6500 - ], - [ - 6001, - 7000 - ], - [ - 6501, - 7500 - ], - [ - 7001, - 8000 - ], - [ - 7501, - 8500 - ], - [ - 8001, - 9000 - ], - [ - 8501, - 9500 - ], - [ - 9001, - 10000 - ], - [ - 9501, - 10500 - ], - [ - 10001, - 11000 - ], - [ - 10501, - 11500 - ], - [ - 11001, - 12000 - ], - [ - 11501, - 12500 - ], - [ - 12001, - 13000 - ], - [ - 12501, - 13500 - ], - [ - 13001, - 14000 - ], - [ - 13501, - 14500 - ], - [ - 14001, - 15000 - ], - [ - 14501, - 15500 - ], - [ - 15001, - 16000 - ], - [ - 15501, - 16500 - ], - [ - 16001, - 17000 - ], - [ - 16501, - 17500 - ], - [ - 17001, - 18000 - ], - [ - 17501, - 18500 - ], - [ - 18001, - 19000 - ], - [ - 18501, - 19500 - ], - [ - 19001, - 20000 - ], - [ - 19501, - 20500 - ], - [ - 20001, - 21000 - ], - [ - 20501, - 21500 - ], - [ - 21001, - 22000 - ], - [ - 21501, - 22500 - ], - [ - 22001, - 23000 - ], - [ - 22501, - 23500 - ], - [ - 23001, - 24000 - ], - [ - 23501, - 24500 - ], - [ - 24001, - 25000 - ] - ], - "used_window_count": 47, - "skipped_window_ranges": [ - [ - 1, - 1000 - ], - [ - 501, - 1500 - ] - ], - "window_samples": 1000, - "effective_hop_s": 1.0e-5, - "energy_threshold": 331.7424505681486, - "skipped_window_count": 2, - "accumulation": "intensity", - "total_window_count": 49, - "hop_samples": 500 - }, - "hasa": { - "effective_window_duration_s": 2.0e-5, - "used_window_ranges": [ - [ - 1001, - 2000 - ], - [ - 1501, - 2500 - ], - [ - 2001, - 3000 - ], - [ - 2501, - 3500 - ], - [ - 3001, - 4000 - ], - [ - 3501, - 4500 - ], - [ - 4001, - 5000 - ], - [ - 4501, - 5500 - ], - [ - 5001, - 6000 - ], - [ - 5501, - 6500 - ], - [ - 6001, - 7000 - ], - [ - 6501, - 7500 - ], - [ - 7001, - 8000 - ], - [ - 7501, - 8500 - ], - [ - 8001, - 9000 - ], - [ - 8501, - 9500 - ], - [ - 9001, - 10000 - ], - [ - 9501, - 10500 - ], - [ - 10001, - 11000 - ], - [ - 10501, - 11500 - ], - [ - 11001, - 12000 - ], - [ - 11501, - 12500 - ], - [ - 12001, - 13000 - ], - [ - 12501, - 13500 - ], - [ - 13001, - 14000 - ], - [ - 13501, - 14500 - ], - [ - 14001, - 15000 - ], - [ - 14501, - 15500 - ], - [ - 15001, - 16000 - ], - [ - 15501, - 16500 - ], - [ - 16001, - 17000 - ], - [ - 16501, - 17500 - ], - [ - 17001, - 18000 - ], - [ - 17501, - 18500 - ], - [ - 18001, - 19000 - ], - [ - 18501, - 19500 - ], - [ - 19001, - 20000 - ], - [ - 19501, - 20500 - ], - [ - 20001, - 21000 - ], - [ - 20501, - 21500 - ], - [ - 21001, - 22000 - ], - [ - 21501, - 22500 - ], - [ - 22001, - 23000 - ], - [ - 22501, - 23500 - ], - [ - 23001, - 24000 - ], - [ - 23501, - 24500 - ], - [ - 24001, - 25000 - ] - ], - "used_window_count": 47, - "skipped_window_ranges": [ - [ - 1, - 1000 - ], - [ - 501, - 1500 - ] - ], - "window_samples": 1000, - "effective_hop_s": 1.0e-5, - "energy_threshold": 331.7424505681486, - "skipped_window_count": 2, - "accumulation": "intensity", - "total_window_count": 49, - "hop_samples": 500 - } - }, - "window_config": { - "window_duration_s": 1.9999999999999998e-5, - "enabled": true, - "hop_s": 9.999999999999999e-6, - "min_energy_ratio": 0.001, - "taper": "hann", - "accumulation": "intensity" - }, - "activity_boundary_figure": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/08_aperture_ap50_dropout0p25_seed42/run/activity_boundaries.png", - "reference_sound_speed_m_per_s": 1551.0647917287392, - "emission_event_count": 21, - "hasa": { - "truth_components": 1, - "true_negative_pixels": 181517, - "energy_outside_predicted_mask": 1.005925109261117e10, - "centroid_depth_mm": 45.4515536423467, - "energy_total": 2.628123453377665e10, - "false_negative_area_mm2": 1.08, - "centroid_error_mm": 0.5779194173109267, - "target_axial_spread_mm": 1.3028997817899934, - "prediction_components": 2, - "false_negative_pixels": 27, - "psf_target_ssim_like": 0.26251948438685924, - "matched_prediction_components": 1, - "recall": 0.968421052631579, - "f1": 0.06868234415826802, - "energy_fraction_outside_predicted_mask": 0.38275413126742647, - "psf_target_mode": "provided_mask", - "energy_outside_mask": 2.5337040121203594e10, - "energy_fraction_inside_predicted_mask": 0.6172458687325735, - "axial_spread_mm": 22.084999758357586, - "false_positive_area_mm2": 897.12, - "missed_truth_components": 0, - "psf_target_normalized_l2_error": 0.9677716399626047, - "psf_target_correlation": 0.28811824166178274, - "true_positive_pixels": 828, - "target_centroid_depth_mm": 44.999999999999915, - "energy_fraction_outside_mask": 0.9640734375944335, - "num_truth_sources": 21, - "recovered_truth_components": 1, - "target_centroid_lateral_mm": -3.6804216213733665e-19, - "psf_axial_fwhm_mm": 0.75, - "energy_inside_mask": 9.441944125730557e8, - "precision": 0.03560371517027864, - "dice": 0.06868234415826802, - "jaccard": 0.03556242752222652, - "centroid_lateral_mm": -0.3606801366700836, - "peak_mm": [ - 44.8, - 4.4 - ], - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "predicted_area_mm2": 930.24, - "overlap_area_mm2": 33.12, - "max_intensity": 1.8147714702623633e6, - "threshold_ratio": 0.2, - "false_positive_pixels": 22428, - "threshold_db": -6.9897000433601875, - "truth_mask_mode": "provided", - "energy_inside_predicted_mask": 1.622198344116548e10, - "truth_radius_mm": 1, - "energy_fraction_inside_mask": 0.035926562405566485, - "psf_target_integral": 854.9999999999999, - "truth_area_mm2": 34.2, - "spurious_prediction_components": 1, - "target_lateral_spread_mm": 3.8127694595420865, - "psf_lateral_fwhm_mm": 0.44999999999999996, - "lateral_spread_mm": 9.26120118169197, - "peak_intensity": 1.8147714702623633e6 - }, - "config": { - "axial_dim": 0.08, - "dt": 2.0e-8, - "bottom_margin": 0.01, - "rho0": 1000, - "success_tolerance": 0.0015, - "dx": 0.0002, - "receiver_aperture": 0.05, - "dz": 0.0002, - "zero_pad_factor": 4, - "transverse_dim": 0.1024, - "t_max": 0.0005, - "c0": 1500, - "peak_suppression_radius": 0.008 - }, - "peak_method": "argmax", - "clean_loop_gain": 0.1, - "geometric": { - "truth_components": 1, - "true_negative_pixels": 162623, - "energy_outside_predicted_mask": 3.6806859874725285e9, - "centroid_depth_mm": 39.98497368237937, - "energy_total": 1.7702316384871536e10, - "false_negative_area_mm2": 1.12, - "centroid_error_mm": 5.031133432923734, - "target_axial_spread_mm": 1.3028997817899934, - "prediction_components": 3, - "false_negative_pixels": 28, - "psf_target_ssim_like": 0.2463457649814433, - "matched_prediction_components": 1, - "recall": 0.9672514619883041, - "f1": 0.03846153846153847, - "energy_fraction_outside_predicted_mask": 0.20792115039916786, - "psf_target_mode": "provided_mask", - "energy_outside_mask": 1.7225245209996456e10, - "energy_fraction_inside_predicted_mask": 0.7920788496008321, - "axial_spread_mm": 23.033218622157943, - "false_positive_area_mm2": 1652.88, - "missed_truth_components": 0, - "psf_target_normalized_l2_error": 0.9770858050085908, - "psf_target_correlation": 0.2293310939130091, - "true_positive_pixels": 827, - "target_centroid_depth_mm": 44.999999999999915, - "energy_fraction_outside_mask": 0.9730503531570147, - "num_truth_sources": 21, - "recovered_truth_components": 1, - "target_centroid_lateral_mm": -3.6804216213733665e-19, - "psf_axial_fwhm_mm": 0.75, - "energy_inside_mask": 4.770711748750819e8, - "precision": 0.01962086882251062, - "dice": 0.038461538461538464, - "jaccard": 0.0196078431372549, - "centroid_lateral_mm": 0.4022619214594711, - "peak_mm": [ - 43.4, - 4.6 - ], - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "predicted_area_mm2": 1685.96, - "overlap_area_mm2": 33.08, - "max_intensity": 903764.8378868633, - "threshold_ratio": 0.2, - "false_positive_pixels": 41322, - "threshold_db": -6.9897000433601875, - "truth_mask_mode": "provided", - "energy_inside_predicted_mask": 1.4021630397399008e10, - "truth_radius_mm": 1, - "energy_fraction_inside_mask": 0.026949646842985397, - "psf_target_integral": 854.9999999999999, - "truth_area_mm2": 34.2, - "spurious_prediction_components": 2, - "target_lateral_spread_mm": 3.8127694595420865, - "psf_lateral_fwhm_mm": 0.44999999999999996, - "lateral_spread_mm": 9.50198739413393, - "peak_intensity": 903764.8378868633 - }, - "clean_max_iter": 500, - "source_variability": { - "amplitude_distribution": "fixed", - "frequency_jitter_percent": 1, - "amplitude_sigma": 0, - "dropout_probability": 0.25 - }, - "emission_meta": { - "emission_event_count": 21, - "random_seed": 42, - "harmonics": [ - 2, - 3, - 4 - ], - "phase_jitter_rad": 0.2, - "n_bubbles_per_cluster": [ - 10 - ], - "cluster_model": "vascular", - "phase_mode": "geometric", - "n_emission_sources": 21, - "cavitation_model": "harmonic_cos", - "gate_duration_s": 4.9999999999999996e-5, - "delays_s": [ - 0 - ], - "activity_model": { - "emission_event_count": 21, - "activity_mode": "static", - "phase_jitter_rad": 0, - "active_probability": 1, - "physical_source_count": 21, - "hop_s": null, - "amplitude_jitter": 0, - "frame_duration_s": null - }, - "transducer_m": [ - -0.03, - 0.0 - ], - "fundamental_hz": 500000, - "harmonic_amplitudes": [ - 1, - 0.6, - 0.3 - ], - "vascular": { - "branch_levels": 2, - "source_spacing_m": 0.0008, - "bundle_count": 3, - "centerlines_m": [ - [ - [ - 0.0465, - -0.006 - ], - [ - 0.046426584774442725, - -0.0056 - ], - [ - 0.04621352549156242, - -0.0052 - ], - [ - 0.045881677878438706, - -0.0048 - ], - [ - 0.04546352549156242, - -0.0044 - ], - [ - 0.045, - -0.004 - ], - [ - 0.044536474508437576, - -0.0036 - ], - [ - 0.04411832212156129, - -0.0032 - ], - [ - 0.043786474508437576, - -0.0028 - ], - [ - 0.043573415225557265, - -0.0024 - ], - [ - 0.0435, - -0.002 - ], - [ - 0.04357341522555727, - -0.0016 - ], - [ - 0.043786474508437576, - -0.0012 - ], - [ - 0.04411832212156129, - -0.0008 - ], - [ - 0.044536474508437576, - -0.0004 - ], - [ - 0.045, - 0.0 - ], - [ - 0.04546352549156242, - 0.0004 - ], - [ - 0.045881677878438706, - 0.0008 - ], - [ - 0.04621352549156242, - 0.0012 - ], - [ - 0.046426584774442725, - 0.0016 - ], - [ - 0.0465, - 0.002 - ], - [ - 0.04642658477444273, - 0.0024 - ], - [ - 0.04621352549156242, - 0.0028 - ], - [ - 0.045881677878438706, - 0.0032 - ], - [ - 0.04546352549156242, - 0.0036 - ], - [ - 0.045, - 0.004 - ], - [ - 0.044536474508437576, - 0.0044 - ], - [ - 0.04411832212156129, - 0.0048 - ], - [ - 0.043786474508437576, - 0.0052 - ], - [ - 0.04357341522555727, - 0.0056 - ], - [ - 0.0435, - 0.006 - ] - ] - ], - "min_separation_m": 0.0003, - "branch_angle_rad": 0.5235987755982988, - "position_jitter_m": 0.00015, - "squiggle_amplitude_m": 0.0015, - "anchors": [ - { - "segments_m": [ - ], - "anchor_m": [ - 0.045, - 0.0 - ], - "source_count": 21, - "centerlines_m": [ - [ - [ - 0.0465, - -0.006 - ], - [ - 0.046426584774442725, - -0.0056 - ], - [ - 0.04621352549156242, - -0.0052 - ], - [ - 0.045881677878438706, - -0.0048 - ], - [ - 0.04546352549156242, - -0.0044 - ], - [ - 0.045, - -0.004 - ], - [ - 0.044536474508437576, - -0.0036 - ], - [ - 0.04411832212156129, - -0.0032 - ], - [ - 0.043786474508437576, - -0.0028 - ], - [ - 0.043573415225557265, - -0.0024 - ], - [ - 0.0435, - -0.002 - ], - [ - 0.04357341522555727, - -0.0016 - ], - [ - 0.043786474508437576, - -0.0012 - ], - [ - 0.04411832212156129, - -0.0008 - ], - [ - 0.044536474508437576, - -0.0004 - ], - [ - 0.045, - 0.0 - ], - [ - 0.04546352549156242, - 0.0004 - ], - [ - 0.045881677878438706, - 0.0008 - ], - [ - 0.04621352549156242, - 0.0012 - ], - [ - 0.046426584774442725, - 0.0016 - ], - [ - 0.0465, - 0.002 - ], - [ - 0.04642658477444273, - 0.0024 - ], - [ - 0.04621352549156242, - 0.0028 - ], - [ - 0.045881677878438706, - 0.0032 - ], - [ - 0.04546352549156242, - 0.0036 - ], - [ - 0.045, - 0.004 - ], - [ - 0.044536474508437576, - 0.0044 - ], - [ - 0.04411832212156129, - 0.0048 - ], - [ - 0.043786474508437576, - 0.0052 - ], - [ - 0.04357341522555727, - 0.0056 - ], - [ - 0.0435, - 0.006 - ] - ] - ] - } - ], - "branch_scale": 0.65, - "topology": "squiggle", - "max_sources_per_anchor": 0, - "truth_radius_m": 0.001, - "squiggle_wavelength_m": 0.008, - "length_m": 0.012, - "squiggle_slope": 0, - "bundle_spacing_m": 0.002 - }, - "n_anchor_clusters": 1, - "physical_source_count": 21, - "anchor_clusters_m": [ - [ - 0.045, - 0.0 - ] - ] - }, - "n_realizations": 1, - "activity_model": { - "emission_event_count": 21, - "activity_mode": "static", - "phase_jitter_rad": 0, - "active_probability": 1, - "physical_source_count": 21, - "hop_s": null, - "amplitude_jitter": 0, - "frame_duration_s": null - }, - "detection_truth_radius_m": 0.001, - "boundary_threshold_ratios": [ - 0.6, - 0.65, - 0.7 - ], - "simulation": { - "receiver_row": 1, - "source_indices": [ - [ - 233, - 227 - ], - [ - 232, - 231 - ], - [ - 230, - 233 - ], - [ - 227, - 236 - ], - [ - 224, - 239 - ], - [ - 221, - 241 - ], - [ - 218, - 244 - ], - [ - 219, - 248 - ], - [ - 221, - 251 - ], - [ - 224, - 254 - ], - [ - 226, - 257 - ], - [ - 229, - 259 - ], - [ - 231, - 263 - ], - [ - 234, - 266 - ], - [ - 233, - 269 - ], - [ - 231, - 273 - ], - [ - 229, - 276 - ], - [ - 226, - 279 - ], - [ - 221, - 280 - ], - [ - 219, - 283 - ], - [ - 218, - 287 - ] - ], - "receiver_cols": [ - 132, - 381 - ] - }, - "reconstruction_frequencies_hz": [ - 1000000, - 1500000, - 2000000 - ], - "activity_boundary_metrics": { - "geometric": [ - { - "target_centroid_lateral_mm": -3.6804216213733665e-19, - "axial_spread_mm": 23.033218622157943, - "peak_intensity": 903764.8378868633, - "dice": 0.20313666915608664, - "false_positive_area_mm2": 158.28, - "energy_inside_predicted_mask": 2.8466623536460786e9, - "overlap_area_mm2": 21.76, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.8391926631658868, - "centroid_lateral_mm": 0.4022619214594711, - "f1": 0.20313666915608664, - "threshold_ratio": 0.6, - "false_positive_pixels": 3957, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.44999999999999996, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.999999999999915, - "true_negative_pixels": 199988, - "energy_outside_predicted_mask": 1.4855654031225458e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 1.7702316384871536e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 1, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899934, - "jaccard": 0.11305070656691604, - "psf_target_correlation": 0.2293310939130091, - "psf_target_ssim_like": 0.2463457649814433, - "precision": 0.12086203065985336, - "peak_mm": [ - 43.4, - 4.6 - ], - "energy_fraction_inside_predicted_mask": 0.16080733683411322, - "recovered_truth_components": 1, - "lateral_spread_mm": 9.50198739413393, - "true_positive_pixels": 544, - "centroid_error_mm": 5.031133432923734, - "recall": 0.6362573099415205, - "matched_prediction_components": 1, - "prediction_components": 2, - "false_negative_pixels": 311, - "energy_fraction_inside_mask": 0.026949646842985397, - "predicted_area_mm2": 180.04, - "energy_fraction_outside_mask": 0.9730503531570147, - "max_intensity": 903764.8378868633, - "energy_inside_mask": 4.770711748750819e8, - "centroid_depth_mm": 39.98497368237937, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8127694595420865, - "psf_target_normalized_l2_error": 0.9770858050085908, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 12.44, - "energy_outside_mask": 1.7225245209996456e10, - "num_truth_sources": 21, - "threshold_db": -2.218487496163564 - }, - { - "target_centroid_lateral_mm": -3.6804216213733665e-19, - "axial_spread_mm": 23.033218622157943, - "peak_intensity": 903764.8378868633, - "dice": 0.18588469184890655, - "false_positive_area_mm2": 111.8, - "energy_inside_predicted_mask": 2.0960305846800995e9, - "overlap_area_mm2": 14.96, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.8815956884336688, - "centroid_lateral_mm": 0.4022619214594711, - "f1": 0.18588469184890657, - "threshold_ratio": 0.65, - "false_positive_pixels": 2795, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.44999999999999996, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.999999999999915, - "true_negative_pixels": 201150, - "energy_outside_predicted_mask": 1.5606285800191437e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 1.7702316384871536e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 0, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899934, - "jaccard": 0.10246575342465754, - "psf_target_correlation": 0.2293310939130091, - "psf_target_ssim_like": 0.2463457649814433, - "precision": 0.11801830230356579, - "peak_mm": [ - 43.4, - 4.6 - ], - "energy_fraction_inside_predicted_mask": 0.11840431156633122, - "recovered_truth_components": 1, - "lateral_spread_mm": 9.50198739413393, - "true_positive_pixels": 374, - "centroid_error_mm": 5.031133432923734, - "recall": 0.43742690058479533, - "matched_prediction_components": 1, - "prediction_components": 1, - "false_negative_pixels": 481, - "energy_fraction_inside_mask": 0.026949646842985397, - "predicted_area_mm2": 126.76, - "energy_fraction_outside_mask": 0.9730503531570147, - "max_intensity": 903764.8378868633, - "energy_inside_mask": 4.770711748750819e8, - "centroid_depth_mm": 39.98497368237937, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8127694595420865, - "psf_target_normalized_l2_error": 0.9770858050085908, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 19.240000000000002, - "energy_outside_mask": 1.7225245209996456e10, - "num_truth_sources": 21, - "threshold_db": -1.8708664335714442 - }, - { - "target_centroid_lateral_mm": -3.6804216213733665e-19, - "axial_spread_mm": 23.033218622157943, - "peak_intensity": 903764.8378868633, - "dice": 0.1426448736998514, - "false_positive_area_mm2": 65.8, - "energy_inside_predicted_mask": 1.2850573090717506e9, - "overlap_area_mm2": 7.68, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.9274073922794666, - "centroid_lateral_mm": 0.4022619214594711, - "f1": 0.1426448736998514, - "threshold_ratio": 0.7, - "false_positive_pixels": 1645, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.44999999999999996, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.999999999999915, - "true_negative_pixels": 202300, - "energy_outside_predicted_mask": 1.6417259075799786e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 1.7702316384871536e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 2, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899934, - "jaccard": 0.0768, - "psf_target_correlation": 0.2293310939130091, - "psf_target_ssim_like": 0.2463457649814433, - "precision": 0.1045182362547632, - "peak_mm": [ - 43.4, - 4.6 - ], - "energy_fraction_inside_predicted_mask": 0.07259260772053341, - "recovered_truth_components": 1, - "lateral_spread_mm": 9.50198739413393, - "true_positive_pixels": 192, - "centroid_error_mm": 5.031133432923734, - "recall": 0.22456140350877193, - "matched_prediction_components": 3, - "prediction_components": 5, - "false_negative_pixels": 663, - "energy_fraction_inside_mask": 0.026949646842985397, - "predicted_area_mm2": 73.48, - "energy_fraction_outside_mask": 0.9730503531570147, - "max_intensity": 903764.8378868633, - "energy_inside_mask": 4.770711748750819e8, - "centroid_depth_mm": 39.98497368237937, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8127694595420865, - "psf_target_normalized_l2_error": 0.9770858050085908, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 26.52, - "energy_outside_mask": 1.7225245209996456e10, - "num_truth_sources": 21, - "threshold_db": -1.5490195998574319 - } - ], - "hasa": [ - { - "target_centroid_lateral_mm": -3.6804216213733665e-19, - "axial_spread_mm": 22.084999758357586, - "peak_intensity": 1.8147714702623633e6, - "dice": 0.2812299807815503, - "false_positive_area_mm2": 73.12, - "energy_inside_predicted_mask": 2.810045196878075e9, - "overlap_area_mm2": 17.56, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.8930778844020206, - "centroid_lateral_mm": -0.3606801366700836, - "f1": 0.2812299807815503, - "threshold_ratio": 0.6, - "false_positive_pixels": 1828, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.44999999999999996, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.999999999999915, - "true_negative_pixels": 202117, - "energy_outside_predicted_mask": 2.3471189336898575e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 2.628123453377665e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 3, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899934, - "jaccard": 0.16362281028699216, - "psf_target_correlation": 0.28811824166178274, - "psf_target_ssim_like": 0.26251948438685924, - "precision": 0.19364799294221438, - "peak_mm": [ - 44.8, - 4.4 - ], - "energy_fraction_inside_predicted_mask": 0.1069221155979794, - "recovered_truth_components": 1, - "lateral_spread_mm": 9.26120118169197, - "true_positive_pixels": 439, - "centroid_error_mm": 0.5779194173109267, - "recall": 0.5134502923976608, - "matched_prediction_components": 2, - "prediction_components": 5, - "false_negative_pixels": 416, - "energy_fraction_inside_mask": 0.035926562405566485, - "predicted_area_mm2": 90.68, - "energy_fraction_outside_mask": 0.9640734375944335, - "max_intensity": 1.8147714702623633e6, - "energy_inside_mask": 9.441944125730557e8, - "centroid_depth_mm": 45.4515536423467, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8127694595420865, - "psf_target_normalized_l2_error": 0.9677716399626047, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 16.64, - "energy_outside_mask": 2.5337040121203594e10, - "num_truth_sources": 21, - "threshold_db": -2.218487496163564 - }, - { - "target_centroid_lateral_mm": -3.6804216213733665e-19, - "axial_spread_mm": 22.084999758357586, - "peak_intensity": 1.8147714702623633e6, - "dice": 0.2874418604651163, - "false_positive_area_mm2": 39.44, - "energy_inside_predicted_mask": 1.7105339337470722e9, - "overlap_area_mm2": 12.36, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.9349142472151112, - "centroid_lateral_mm": -0.3606801366700836, - "f1": 0.2874418604651163, - "threshold_ratio": 0.65, - "false_positive_pixels": 986, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.44999999999999996, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.999999999999915, - "true_negative_pixels": 202959, - "energy_outside_predicted_mask": 2.457070060002958e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 2.628123453377665e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 4, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899934, - "jaccard": 0.16784356328082564, - "psf_target_correlation": 0.28811824166178274, - "psf_target_ssim_like": 0.26251948438685924, - "precision": 0.2386100386100386, - "peak_mm": [ - 44.8, - 4.4 - ], - "energy_fraction_inside_predicted_mask": 0.06508575278488891, - "recovered_truth_components": 1, - "lateral_spread_mm": 9.26120118169197, - "true_positive_pixels": 309, - "centroid_error_mm": 0.5779194173109267, - "recall": 0.36140350877192984, - "matched_prediction_components": 3, - "prediction_components": 7, - "false_negative_pixels": 546, - "energy_fraction_inside_mask": 0.035926562405566485, - "predicted_area_mm2": 51.800000000000004, - "energy_fraction_outside_mask": 0.9640734375944335, - "max_intensity": 1.8147714702623633e6, - "energy_inside_mask": 9.441944125730557e8, - "centroid_depth_mm": 45.4515536423467, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8127694595420865, - "psf_target_normalized_l2_error": 0.9677716399626047, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 21.84, - "energy_outside_mask": 2.5337040121203594e10, - "num_truth_sources": 21, - "threshold_db": -1.8708664335714442 - }, - { - "target_centroid_lateral_mm": -3.6804216213733665e-19, - "axial_spread_mm": 22.084999758357586, - "peak_intensity": 1.8147714702623633e6, - "dice": 0.2648037258815702, - "false_positive_area_mm2": 17.96, - "energy_inside_predicted_mask": 9.232269908593438e8, - "overlap_area_mm2": 7.96, - "psf_target_mode": "provided_mask", - "energy_fraction_outside_predicted_mask": 0.9648712472135655, - "centroid_lateral_mm": -0.3606801366700836, - "f1": 0.2648037258815702, - "threshold_ratio": 0.7, - "false_positive_pixels": 449, - "missed_truth_components": 0, - "psf_lateral_fwhm_mm": 0.44999999999999996, - "truth_mask_mode": "provided", - "target_centroid_depth_mm": 44.999999999999915, - "true_negative_pixels": 203496, - "energy_outside_predicted_mask": 2.5358007542917305e10, - "truth_radius_mm": 1, - "truth_mm": [ - [ - 46.32144028763879, - -6.032772503896101 - ], - [ - 46.26569514702229, - -5.272737898667883 - ], - [ - 45.72226666640846, - -4.73126942728387 - ], - [ - 45.166195362304585, - -4.200866487451935 - ], - [ - 44.61734980426383, - -3.656508390245649 - ], - [ - 43.98719954353103, - -3.1861889638953005 - ], - [ - 43.48928418531734, - -2.56652716942303 - ], - [ - 43.554350891479686, - -1.7486279796490285 - ], - [ - 43.959799624315366, - -1.125078113947233 - ], - [ - 44.51058220522799, - -0.6028161597729909 - ], - [ - 45.020721921306134, - -0.02401284689886321 - ], - [ - 45.66686911546983, - 0.4173119265126402 - ], - [ - 46.05530266618959, - 1.1125489663053103 - ], - [ - 46.64759181635218, - 1.7115638060362217 - ], - [ - 46.3526618538167, - 2.4823400105268 - ], - [ - 45.97133599677839, - 3.1517892589508425 - ], - [ - 45.56339958483016, - 3.8659632638191512 - ], - [ - 44.92980070620905, - 4.31210804956654 - ], - [ - 44.08954562520898, - 4.534541578681507 - ], - [ - 43.667310276652834, - 5.237053357696269 - ], - [ - 43.3644115625137, - 5.9751143606974635 - ] - ], - "energy_total": 2.628123453377665e10, - "psf_target_integral": 854.9999999999999, - "spurious_prediction_components": 5, - "truth_components": 1, - "target_axial_spread_mm": 1.3028997817899934, - "jaccard": 0.1526073619631902, - "psf_target_correlation": 0.28811824166178274, - "psf_target_ssim_like": 0.26251948438685924, - "precision": 0.30709876543209874, - "peak_mm": [ - 44.8, - 4.4 - ], - "energy_fraction_inside_predicted_mask": 0.03512875278643445, - "recovered_truth_components": 1, - "lateral_spread_mm": 9.26120118169197, - "true_positive_pixels": 199, - "centroid_error_mm": 0.5779194173109267, - "recall": 0.2327485380116959, - "matched_prediction_components": 2, - "prediction_components": 7, - "false_negative_pixels": 656, - "energy_fraction_inside_mask": 0.035926562405566485, - "predicted_area_mm2": 25.92, - "energy_fraction_outside_mask": 0.9640734375944335, - "max_intensity": 1.8147714702623633e6, - "energy_inside_mask": 9.441944125730557e8, - "centroid_depth_mm": 45.4515536423467, - "truth_area_mm2": 34.2, - "target_lateral_spread_mm": 3.8127694595420865, - "psf_target_normalized_l2_error": 0.9677716399626047, - "psf_axial_fwhm_mm": 0.75, - "false_negative_area_mm2": 26.240000000000002, - "energy_outside_mask": 2.5337040121203594e10, - "num_truth_sources": 21, - "threshold_db": -1.5490195998574319 - } - ], - "threshold_ratios": [ - 0.6, - 0.65, - 0.7 - ] - } -} \ No newline at end of file diff --git a/runs/20260506_103348_pam_aperture_sweep/08_aperture_ap50_dropout0p25_seed42/status.json b/runs/20260506_103348_pam_aperture_sweep/08_aperture_ap50_dropout0p25_seed42/status.json deleted file mode 100644 index 3a23a64..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/08_aperture_ap50_dropout0p25_seed42/status.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "best_hasa_f1": 0.2874418604651163, - "best_hasa_recall": 0.36140350877192984, - "hasa_psf_corr": 0.28811824166178274, - "best_geo_threshold": 0.6, - "hasa_centroid_error_mm": 0.5779194173109267, - "best_hasa_jaccard": 0.16784356328082564, - "summary_found": true, - "kind": "simulation", - "best_hasa_precision": 0.2386100386100386, - "status": "success", - "elapsed_min": 4.09, - "id": "aperture_ap50_dropout0p25_seed42", - "best_geo_f1": 0.20313666915608664, - "cmd": "`/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.25 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/08_aperture_ap50_dropout0p25_seed42/run' --random-seed=42 --receiver-aperture-mm=50 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle`", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/08_aperture_ap50_dropout0p25_seed42/run", - "hasa_psf_l2": 0.9677716399626047, - "best_hasa_threshold": 0.65 -} diff --git a/runs/20260506_103348_pam_aperture_sweep/09_aperture_ap60_dropout0p25_seed42/run.log b/runs/20260506_103348_pam_aperture_sweep/09_aperture_ap60_dropout0p25_seed42/run.log deleted file mode 100644 index 309f3a7..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/09_aperture_ap60_dropout0p25_seed42/run.log +++ /dev/null @@ -1,4 +0,0 @@ -# 2026-05-06T11:14:00.473 -# `/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.25 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/09_aperture_ap60_dropout0p25_seed42/run' --random-seed=42 --receiver-aperture-mm=60 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle` -WARNING:root:Highest prime factors in each dimension are [11 23] -WARNING:root:Use dimension sizes with lower prime factors to improve speed diff --git a/runs/20260506_103348_pam_aperture_sweep/09_aperture_ap60_dropout0p25_seed42/status.json b/runs/20260506_103348_pam_aperture_sweep/09_aperture_ap60_dropout0p25_seed42/status.json deleted file mode 100644 index f6042fe..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/09_aperture_ap60_dropout0p25_seed42/status.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "status": "running", - "elapsed_min": 0, - "id": "aperture_ap60_dropout0p25_seed42", - "cmd": "`/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.25 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/09_aperture_ap60_dropout0p25_seed42/run' --random-seed=42 --receiver-aperture-mm=60 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle`", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/09_aperture_ap60_dropout0p25_seed42/run" -} diff --git a/runs/20260506_103348_pam_aperture_sweep/leaderboard.json b/runs/20260506_103348_pam_aperture_sweep/leaderboard.json deleted file mode 100644 index 38debe7..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/leaderboard.json +++ /dev/null @@ -1,162 +0,0 @@ -[ - { - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "best_hasa_f1": 0.42812500000000003, - "best_hasa_recall": 0.4807017543859649, - "hasa_psf_corr": 0.33207505631287954, - "best_geo_threshold": 0.7, - "hasa_centroid_error_mm": 1.2644119391633832, - "best_hasa_jaccard": 0.27236580516898606, - "summary_found": true, - "kind": "simulation", - "best_hasa_precision": 0.38591549295774646, - "status": "success", - "elapsed_min": 4.67, - "id": "aperture_apfull_dropout0p0_seed42", - "best_geo_f1": 0.3301797540208136, - "cmd": "`/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/04_aperture_apfull_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=full --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle`", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/04_aperture_apfull_dropout0p0_seed42/run", - "hasa_psf_l2": 0.9663008170100353, - "best_hasa_threshold": 0.65 - }, - { - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "best_hasa_f1": 0.4281217208814271, - "best_hasa_recall": 0.47719298245614034, - "hasa_psf_corr": 0.3321059725982636, - "best_geo_threshold": 0.7, - "hasa_centroid_error_mm": 1.2868821478349797, - "best_hasa_jaccard": 0.27236315086782376, - "summary_found": true, - "kind": "simulation", - "best_hasa_precision": 0.3882017126546147, - "status": "success", - "elapsed_min": 6.34, - "id": "aperture_ap100_dropout0p0_seed42", - "best_geo_f1": 0.3162016642192854, - "cmd": "`/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/03_aperture_ap100_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=100 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle`", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/03_aperture_ap100_dropout0p0_seed42/run", - "hasa_psf_l2": 0.9661709715544975, - "best_hasa_threshold": 0.65 - }, - { - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "best_hasa_f1": 0.36548715462918085, - "best_hasa_recall": 0.4409356725146199, - "hasa_psf_corr": 0.3025676051477649, - "best_geo_threshold": 0.65, - "hasa_centroid_error_mm": 1.1328610292087822, - "best_hasa_jaccard": 0.2236061684460261, - "summary_found": true, - "kind": "simulation", - "best_hasa_precision": 0.3120860927152318, - "status": "success", - "elapsed_min": 6.42, - "id": "aperture_ap60_dropout0p0_seed42", - "best_geo_f1": 0.2620552045227802, - "cmd": "`/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/02_aperture_ap60_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=60 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle`", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/02_aperture_ap60_dropout0p0_seed42/run", - "hasa_psf_l2": 0.9666656097357209, - "best_hasa_threshold": 0.65 - }, - { - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "best_hasa_f1": 0.359705611775529, - "best_hasa_recall": 0.45730994152046783, - "hasa_psf_corr": 0.2917420463453979, - "best_geo_threshold": 0.7, - "hasa_centroid_error_mm": 0.9850847735466974, - "best_hasa_jaccard": 0.21929332585530006, - "summary_found": true, - "kind": "simulation", - "best_hasa_precision": 0.2964366944655042, - "status": "success", - "elapsed_min": 6.42, - "id": "aperture_ap50_dropout0p0_seed42", - "best_geo_f1": 0.23684210526315785, - "cmd": "`/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/01_aperture_ap50_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=50 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle`", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/01_aperture_ap50_dropout0p0_seed42/run", - "hasa_psf_l2": 0.967300772825553, - "best_hasa_threshold": 0.65 - }, - { - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "best_hasa_f1": 0.2874418604651163, - "best_hasa_recall": 0.36140350877192984, - "hasa_psf_corr": 0.28811824166178274, - "best_geo_threshold": 0.6, - "hasa_centroid_error_mm": 0.5779194173109267, - "best_hasa_jaccard": 0.16784356328082564, - "summary_found": true, - "kind": "simulation", - "best_hasa_precision": 0.2386100386100386, - "status": "success", - "elapsed_min": 4.09, - "id": "aperture_ap50_dropout0p25_seed42", - "best_geo_f1": 0.20313666915608664, - "cmd": "`/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.25 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/08_aperture_ap50_dropout0p25_seed42/run' --random-seed=42 --receiver-aperture-mm=50 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle`", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/08_aperture_ap50_dropout0p25_seed42/run", - "hasa_psf_l2": 0.9677716399626047, - "best_hasa_threshold": 0.65 - }, - { - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "best_hasa_f1": 0.28295819935691324, - "best_hasa_recall": 0.5146198830409356, - "hasa_psf_corr": 0.2651437853373778, - "best_geo_threshold": 0.7, - "hasa_centroid_error_mm": 0.7121510998832777, - "best_hasa_jaccard": 0.1647940074906367, - "summary_found": true, - "kind": "simulation", - "best_hasa_precision": 0.1951219512195122, - "status": "success", - "elapsed_min": 4.09, - "id": "aperture_ap40_dropout0p0_seed42", - "best_geo_f1": 0.1925061700695535, - "cmd": "`/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/05_aperture_ap40_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=40 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle`", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/05_aperture_ap40_dropout0p0_seed42/run", - "hasa_psf_l2": 0.9701594791109309, - "best_hasa_threshold": 0.65 - }, - { - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "best_hasa_f1": 0.17551190973673214, - "best_hasa_recall": 0.24561403508771928, - "hasa_psf_corr": 0.25496439085812905, - "best_geo_threshold": 0.6, - "hasa_centroid_error_mm": 0.7173663523393065, - "best_hasa_jaccard": 0.0961978928080623, - "summary_found": true, - "kind": "simulation", - "best_hasa_precision": 0.13654096228868662, - "status": "success", - "elapsed_min": 4.09, - "id": "aperture_ap30_dropout0p0_seed42", - "best_geo_f1": 0.15389082462253192, - "cmd": "`/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/06_aperture_ap30_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=30 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle`", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/06_aperture_ap30_dropout0p0_seed42/run", - "hasa_psf_l2": 0.9704168611999944, - "best_hasa_threshold": 0.6 - }, - { - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "best_hasa_f1": 0.018018018018018018, - "best_hasa_recall": 0.0152046783625731, - "hasa_psf_corr": 0.24714886487972174, - "best_geo_threshold": 0.6, - "hasa_centroid_error_mm": 0.6298582959424722, - "best_hasa_jaccard": 0.00909090909090909, - "summary_found": true, - "kind": "simulation", - "best_hasa_precision": 0.022108843537414966, - "status": "success", - "elapsed_min": 4.09, - "id": "aperture_ap20_dropout0p0_seed42", - "best_geo_f1": 0.12082853855005753, - "cmd": "`/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/07_aperture_ap20_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=20 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle`", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/07_aperture_ap20_dropout0p0_seed42/run", - "hasa_psf_l2": 0.9706298799608375, - "best_hasa_threshold": 0.6 - } -] diff --git a/runs/20260506_103348_pam_aperture_sweep/manifest.json b/runs/20260506_103348_pam_aperture_sweep/manifest.json deleted file mode 100644 index 9910b0d..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/manifest.json +++ /dev/null @@ -1,2130 +0,0 @@ -{ - "created_at": "2026-05-06T10:33:48.177", - "max_hours": 8.75, - "per_run_timeout_min": 90, - "jobs": [ - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap50_dropout0p0_seed42", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "50", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "42", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap60_dropout0p0_seed42", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "60", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "42", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap100_dropout0p0_seed42", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "100", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "42", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_apfull_dropout0p0_seed42", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "full", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "42", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap40_dropout0p0_seed42", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "40", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "42", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap30_dropout0p0_seed42", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "30", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "42", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap20_dropout0p0_seed42", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "20", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "42", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap50_dropout0p25_seed42", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "50", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "42", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap60_dropout0p25_seed42", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "60", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "42", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap100_dropout0p25_seed42", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "100", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "42", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_apfull_dropout0p25_seed42", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "full", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "42", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap40_dropout0p25_seed42", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "40", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "42", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap30_dropout0p25_seed42", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "30", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "42", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap20_dropout0p25_seed42", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "20", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "42", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap50_dropout0p0_seed43", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "50", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "43", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap60_dropout0p0_seed43", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "60", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "43", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap100_dropout0p0_seed43", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "100", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "43", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_apfull_dropout0p0_seed43", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "full", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "43", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap40_dropout0p0_seed43", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "40", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "43", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap30_dropout0p0_seed43", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "30", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "43", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap20_dropout0p0_seed43", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "20", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "43", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap50_dropout0p25_seed43", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "50", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "43", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap60_dropout0p25_seed43", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "60", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "43", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap100_dropout0p25_seed43", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "100", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "43", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_apfull_dropout0p25_seed43", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "full", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "43", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap40_dropout0p25_seed43", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "40", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "43", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap30_dropout0p25_seed43", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "30", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "43", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap20_dropout0p25_seed43", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "20", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "43", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap50_dropout0p0_seed44", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "50", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "44", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap60_dropout0p0_seed44", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "60", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "44", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap100_dropout0p0_seed44", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "100", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "44", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_apfull_dropout0p0_seed44", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "full", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "44", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap40_dropout0p0_seed44", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "40", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "44", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap30_dropout0p0_seed44", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "30", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "44", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap20_dropout0p0_seed44", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "20", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "44", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap50_dropout0p25_seed44", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "50", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "44", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap60_dropout0p25_seed44", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "60", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "44", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap100_dropout0p25_seed44", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "100", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "44", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_apfull_dropout0p25_seed44", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "full", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "44", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap40_dropout0p25_seed44", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "40", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "44", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap30_dropout0p25_seed44", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "30", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "44", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap20_dropout0p25_seed44", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "20", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "44", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap50_dropout0p0_seed45", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "50", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "45", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap60_dropout0p0_seed45", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "60", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "45", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap100_dropout0p0_seed45", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "100", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "45", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_apfull_dropout0p0_seed45", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "full", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "45", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap40_dropout0p0_seed45", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "40", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "45", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap30_dropout0p0_seed45", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "30", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "45", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap20_dropout0p0_seed45", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "20", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "45", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap50_dropout0p25_seed45", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "50", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "45", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap60_dropout0p25_seed45", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "60", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "45", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap100_dropout0p25_seed45", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "100", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "45", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_apfull_dropout0p25_seed45", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "full", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "45", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap40_dropout0p25_seed45", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "40", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "45", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap30_dropout0p25_seed45", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "30", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "45", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap20_dropout0p25_seed45", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "20", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "45", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap50_dropout0p0_seed46", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "50", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "46", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap60_dropout0p0_seed46", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "60", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "46", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap100_dropout0p0_seed46", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "100", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "46", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_apfull_dropout0p0_seed46", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "full", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "46", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap40_dropout0p0_seed46", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "40", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "46", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap30_dropout0p0_seed46", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "30", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "46", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap20_dropout0p0_seed46", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.0", - "receiver-aperture-mm": "20", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "46", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap50_dropout0p25_seed46", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "50", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "46", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap60_dropout0p25_seed46", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "60", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "46", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap100_dropout0p25_seed46", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "100", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "46", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_apfull_dropout0p25_seed46", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "full", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "46", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap40_dropout0p25_seed46", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "40", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "46", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap30_dropout0p25_seed46", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "30", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "46", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - }, - { - "kind": "simulation", - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "id": "aperture_ap20_dropout0p25_seed46", - "args": { - "recon-window-us": "20", - "slice-index": "250", - "source-phase-mode": "random_phase_per_window", - "skull-transducer-distance-mm": "30", - "harmonics": "2,3,4", - "harmonic-amplitudes": "1.0,0.6,0.3", - "dropout-probability": "0.25", - "receiver-aperture-mm": "20", - "aberrator": "skull", - "cavitation-model": "harmonic-cos", - "transverse-mm": "102.4", - "frequency-jitter-percent": "1", - "recon-hop-us": "10", - "vascular-topology": "squiggle", - "cluster-model": "vascular", - "recon-bandwidth-khz": "500", - "random-seed": "46", - "vascular-length-mm": "12", - "t-max-us": "500", - "recon-min-window-energy-ratio": "0.001", - "clusters-mm": "45:0", - "activity-mode": "static", - "boundary-threshold-ratios": "0.6,0.65,0.7" - } - } - ], - "apertures_mm": [ - "50", - "60", - "100", - "full", - "40", - "30", - "20" - ], - "output_root": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep", - "dropout_probabilities": [ - "0.0", - "0.25" - ], - "simulation_random_seeds": [ - "42", - "43", - "44", - "45", - "46" - ], - "job_count": 70, - "transverse_mm": "102.4" -} diff --git a/runs/20260506_103348_pam_aperture_sweep/results.csv b/runs/20260506_103348_pam_aperture_sweep/results.csv deleted file mode 100644 index 80bd2fd..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/results.csv +++ /dev/null @@ -1,9 +0,0 @@ -id,kind,status,elapsed_min,best_hasa_f1,best_hasa_threshold,best_hasa_precision,best_hasa_recall,best_hasa_jaccard,best_geo_f1,hasa_psf_corr,hasa_psf_l2,out_dir -aperture_ap50_dropout0p0_seed42,simulation,success,6.42,0.359705611775529,0.65,0.2964366944655042,0.45730994152046783,0.21929332585530006,0.23684210526315785,0.2917420463453979,0.967300772825553,/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/01_aperture_ap50_dropout0p0_seed42/run -aperture_ap60_dropout0p0_seed42,simulation,success,6.42,0.36548715462918085,0.65,0.3120860927152318,0.4409356725146199,0.2236061684460261,0.2620552045227802,0.3025676051477649,0.9666656097357209,/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/02_aperture_ap60_dropout0p0_seed42/run -aperture_ap100_dropout0p0_seed42,simulation,success,6.34,0.4281217208814271,0.65,0.3882017126546147,0.47719298245614034,0.27236315086782376,0.3162016642192854,0.3321059725982636,0.9661709715544975,/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/03_aperture_ap100_dropout0p0_seed42/run -aperture_apfull_dropout0p0_seed42,simulation,success,4.67,0.42812500000000003,0.65,0.38591549295774646,0.4807017543859649,0.27236580516898606,0.3301797540208136,0.33207505631287954,0.9663008170100353,/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/04_aperture_apfull_dropout0p0_seed42/run -aperture_ap40_dropout0p0_seed42,simulation,success,4.09,0.28295819935691324,0.65,0.1951219512195122,0.5146198830409356,0.1647940074906367,0.1925061700695535,0.2651437853373778,0.9701594791109309,/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/05_aperture_ap40_dropout0p0_seed42/run -aperture_ap30_dropout0p0_seed42,simulation,success,4.09,0.17551190973673214,0.6,0.13654096228868662,0.24561403508771928,0.0961978928080623,0.15389082462253192,0.25496439085812905,0.9704168611999944,/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/06_aperture_ap30_dropout0p0_seed42/run -aperture_ap20_dropout0p0_seed42,simulation,success,4.09,0.018018018018018018,0.6,0.022108843537414966,0.0152046783625731,0.00909090909090909,0.12082853855005753,0.24714886487972174,0.9706298799608375,/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/07_aperture_ap20_dropout0p0_seed42/run -aperture_ap50_dropout0p25_seed42,simulation,success,4.09,0.2874418604651163,0.65,0.2386100386100386,0.36140350877192984,0.16784356328082564,0.20313666915608664,0.28811824166178274,0.9677716399626047,/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/08_aperture_ap50_dropout0p25_seed42/run diff --git a/runs/20260506_103348_pam_aperture_sweep/results.json b/runs/20260506_103348_pam_aperture_sweep/results.json deleted file mode 100644 index 13257e9..0000000 --- a/runs/20260506_103348_pam_aperture_sweep/results.json +++ /dev/null @@ -1,162 +0,0 @@ -[ - { - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "best_hasa_f1": 0.359705611775529, - "best_hasa_recall": 0.45730994152046783, - "hasa_psf_corr": 0.2917420463453979, - "best_geo_threshold": 0.7, - "hasa_centroid_error_mm": 0.9850847735466974, - "best_hasa_jaccard": 0.21929332585530006, - "summary_found": true, - "kind": "simulation", - "best_hasa_precision": 0.2964366944655042, - "status": "success", - "elapsed_min": 6.42, - "id": "aperture_ap50_dropout0p0_seed42", - "best_geo_f1": 0.23684210526315785, - "cmd": "`/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/01_aperture_ap50_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=50 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle`", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/01_aperture_ap50_dropout0p0_seed42/run", - "hasa_psf_l2": 0.967300772825553, - "best_hasa_threshold": 0.65 - }, - { - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "best_hasa_f1": 0.36548715462918085, - "best_hasa_recall": 0.4409356725146199, - "hasa_psf_corr": 0.3025676051477649, - "best_geo_threshold": 0.65, - "hasa_centroid_error_mm": 1.1328610292087822, - "best_hasa_jaccard": 0.2236061684460261, - "summary_found": true, - "kind": "simulation", - "best_hasa_precision": 0.3120860927152318, - "status": "success", - "elapsed_min": 6.42, - "id": "aperture_ap60_dropout0p0_seed42", - "best_geo_f1": 0.2620552045227802, - "cmd": "`/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/02_aperture_ap60_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=60 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle`", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/02_aperture_ap60_dropout0p0_seed42/run", - "hasa_psf_l2": 0.9666656097357209, - "best_hasa_threshold": 0.65 - }, - { - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "best_hasa_f1": 0.4281217208814271, - "best_hasa_recall": 0.47719298245614034, - "hasa_psf_corr": 0.3321059725982636, - "best_geo_threshold": 0.7, - "hasa_centroid_error_mm": 1.2868821478349797, - "best_hasa_jaccard": 0.27236315086782376, - "summary_found": true, - "kind": "simulation", - "best_hasa_precision": 0.3882017126546147, - "status": "success", - "elapsed_min": 6.34, - "id": "aperture_ap100_dropout0p0_seed42", - "best_geo_f1": 0.3162016642192854, - "cmd": "`/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/03_aperture_ap100_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=100 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle`", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/03_aperture_ap100_dropout0p0_seed42/run", - "hasa_psf_l2": 0.9661709715544975, - "best_hasa_threshold": 0.65 - }, - { - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "best_hasa_f1": 0.42812500000000003, - "best_hasa_recall": 0.4807017543859649, - "hasa_psf_corr": 0.33207505631287954, - "best_geo_threshold": 0.7, - "hasa_centroid_error_mm": 1.2644119391633832, - "best_hasa_jaccard": 0.27236580516898606, - "summary_found": true, - "kind": "simulation", - "best_hasa_precision": 0.38591549295774646, - "status": "success", - "elapsed_min": 4.67, - "id": "aperture_apfull_dropout0p0_seed42", - "best_geo_f1": 0.3301797540208136, - "cmd": "`/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/04_aperture_apfull_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=full --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle`", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/04_aperture_apfull_dropout0p0_seed42/run", - "hasa_psf_l2": 0.9663008170100353, - "best_hasa_threshold": 0.65 - }, - { - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "best_hasa_f1": 0.28295819935691324, - "best_hasa_recall": 0.5146198830409356, - "hasa_psf_corr": 0.2651437853373778, - "best_geo_threshold": 0.7, - "hasa_centroid_error_mm": 0.7121510998832777, - "best_hasa_jaccard": 0.1647940074906367, - "summary_found": true, - "kind": "simulation", - "best_hasa_precision": 0.1951219512195122, - "status": "success", - "elapsed_min": 4.09, - "id": "aperture_ap40_dropout0p0_seed42", - "best_geo_f1": 0.1925061700695535, - "cmd": "`/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/05_aperture_ap40_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=40 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle`", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/05_aperture_ap40_dropout0p0_seed42/run", - "hasa_psf_l2": 0.9701594791109309, - "best_hasa_threshold": 0.65 - }, - { - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "best_hasa_f1": 0.17551190973673214, - "best_hasa_recall": 0.24561403508771928, - "hasa_psf_corr": 0.25496439085812905, - "best_geo_threshold": 0.6, - "hasa_centroid_error_mm": 0.7173663523393065, - "best_hasa_jaccard": 0.0961978928080623, - "summary_found": true, - "kind": "simulation", - "best_hasa_precision": 0.13654096228868662, - "status": "success", - "elapsed_min": 4.09, - "id": "aperture_ap30_dropout0p0_seed42", - "best_geo_f1": 0.15389082462253192, - "cmd": "`/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/06_aperture_ap30_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=30 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle`", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/06_aperture_ap30_dropout0p0_seed42/run", - "hasa_psf_l2": 0.9704168611999944, - "best_hasa_threshold": 0.6 - }, - { - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "best_hasa_f1": 0.018018018018018018, - "best_hasa_recall": 0.0152046783625731, - "hasa_psf_corr": 0.24714886487972174, - "best_geo_threshold": 0.6, - "hasa_centroid_error_mm": 0.6298582959424722, - "best_hasa_jaccard": 0.00909090909090909, - "summary_found": true, - "kind": "simulation", - "best_hasa_precision": 0.022108843537414966, - "status": "success", - "elapsed_min": 4.09, - "id": "aperture_ap20_dropout0p0_seed42", - "best_geo_f1": 0.12082853855005753, - "cmd": "`/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.0 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/07_aperture_ap20_dropout0p0_seed42/run' --random-seed=42 --receiver-aperture-mm=20 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle`", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/07_aperture_ap20_dropout0p0_seed42/run", - "hasa_psf_l2": 0.9706298799608375, - "best_hasa_threshold": 0.6 - }, - { - "note": "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a 102.4 mm transverse grid.", - "best_hasa_f1": 0.2874418604651163, - "best_hasa_recall": 0.36140350877192984, - "hasa_psf_corr": 0.28811824166178274, - "best_geo_threshold": 0.6, - "hasa_centroid_error_mm": 0.5779194173109267, - "best_hasa_jaccard": 0.16784356328082564, - "summary_found": true, - "kind": "simulation", - "best_hasa_precision": 0.2386100386100386, - "status": "success", - "elapsed_min": 4.09, - "id": "aperture_ap50_dropout0p25_seed42", - "best_geo_f1": 0.20313666915608664, - "cmd": "`/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/bin/julia -C native -J/Users/vm/.julia/juliaup/julia-1.12.2+0.aarch64.apple.darwin14/lib/julia/sys.dylib -g1 '--project=/Users/vm/INI_code/Julia II/' '/Users/vm/INI_code/Julia II/scripts/run_pam_clusters.jl' --aberrator=skull --activity-mode=static --boundary-threshold-ratios=0.6,0.65,0.7 --cavitation-model=harmonic-cos --cluster-model=vascular --clusters-mm=45:0 --dropout-probability=0.25 --frequency-jitter-percent=1 --harmonic-amplitudes=1.0,0.6,0.3 --harmonics=2,3,4 '--out-dir=/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/08_aperture_ap50_dropout0p25_seed42/run' --random-seed=42 --receiver-aperture-mm=50 --recon-bandwidth-khz=500 --recon-hop-us=10 --recon-min-window-energy-ratio=0.001 --recon-window-us=20 --skull-transducer-distance-mm=30 --slice-index=250 --source-phase-mode=random_phase_per_window --t-max-us=500 --transverse-mm=102.4 --vascular-length-mm=12 --vascular-topology=squiggle`", - "out_dir": "/Users/vm/INI_code/Julia II/outputs/20260506_103348_pam_aperture_sweep/08_aperture_ap50_dropout0p25_seed42/run", - "hasa_psf_l2": 0.9677716399626047, - "best_hasa_threshold": 0.65 - } -] diff --git a/scripts/compare_estimators.jl b/scripts/compare_focus_estimators.jl similarity index 99% rename from scripts/compare_estimators.jl rename to scripts/compare_focus_estimators.jl index 229b080..ded556e 100644 --- a/scripts/compare_estimators.jl +++ b/scripts/compare_focus_estimators.jl @@ -37,7 +37,7 @@ function default_output_dir(opts, placement_mode, focus_depth) timestamp = Dates.format(Dates.now(), "yyyymmdd_HHMMSS") parts = String[ timestamp, - "compare_estimators", + "compare_focus_estimators", lowercase(opts["medium"]), String(placement_mode), "f$(slug_value(parse(Float64, opts["frequency-mhz"]); digits=2))mhz", diff --git a/scripts/run_focus_case.jl b/scripts/run_focus.jl similarity index 100% rename from scripts/run_focus_case.jl rename to scripts/run_focus.jl diff --git a/scripts/run_pam.jl b/scripts/run_pam.jl index 30b4348..4ab3fdd 100644 --- a/scripts/run_pam.jl +++ b/scripts/run_pam.jl @@ -1,2094 +1,480 @@ #!/usr/bin/env julia -using Pkg -Pkg.activate(joinpath(@__DIR__, "..")) +if abspath(PROGRAM_FILE) == @__FILE__ + import Pkg + Pkg.activate(joinpath(@__DIR__, "..")) +end -using Dates -using Printf using Random -using Statistics -using CairoMakie using JLD2 using JSON3 using TranscranialFUS - -function parse_cli(args) - opts = Dict{String, String}( - "dimension" => "2", - "source-model" => "squiggle", - "sources-mm" => "30:0", - "anchors-mm" => "45:0", - "frequency-mhz" => "0.4", - "fundamental-mhz" => "0.5", - "amplitude-pa" => "1.0", - "source-amplitudes-pa" => "", - "source-frequencies-mhz" => "", - "phases-deg" => "", - "n-bubbles" => "10", - "num-cycles" => "4", - "harmonics" => "2,3,4", - "harmonic-amplitudes" => "1.0,0.6,0.3", - "cavitation-model" => "harmonic-cos", - "gate-us" => "50", - "taper-ratio" => "0.25", - "axial-mm" => "80", - "transverse-mm" => "102.4", - "dx-mm" => "0.2", - "dz-mm" => "0.2", - "dy-mm" => "", - "transverse-y-mm" => "", - "transverse-z-mm" => "", - "axial-gain-power" => "1.5", - "receiver-aperture-mm" => "full", - "receiver-aperture-y-mm" => "", - "receiver-aperture-z-mm" => "", - "t-max-us" => "500", - "dt-ns" => "20", - "zero-pad-factor" => "4", - "peak-suppression-radius-mm" => "8.0", - "success-tolerance-mm" => "1.5", - "aberrator" => "none", - "sim-mode" => "auto", - "ct-path" => DEFAULT_CT_PATH, - "slice-index" => "250", - "skull-transducer-distance-mm" => "30", - "bottom-margin-mm" => "10", - "hu-bone-thr" => "200", - "lens-depth-mm" => "12", - "lens-lateral-mm" => "0", - "lens-axial-radius-mm" => "3", - "lens-lateral-radius-mm" => "12", - "aberrator-c" => "1700", - "aberrator-rho" => "1150", - "use-gpu" => "false", - "recon-bandwidth-khz" => "500", - "recon-step-um" => "50", - "recon-mode" => "auto", - "recon-window-us" => "20", - "recon-hop-us" => "10", - "recon-window-taper" => "hann", - "recon-min-window-energy-ratio" => "0.001", - "recon-progress" => "false", - "benchmark" => "false", - "window-batch" => "1", - "phase-mode" => "geometric", - "phase-jitter-rad" => "0.2", - "random-seed" => "42", - "source-phase-mode" => "random_phase_per_window", - "n-realizations" => "1", - "frequency-jitter-percent" => "1", - "transducer-mm" => "-30:0", - "delays-us" => "0", - "vascular-length-mm" => "12", - "vascular-squiggle-amplitude-mm" => "1.5", - "vascular-squiggle-amplitude-x-mm" => "1.0", - "vascular-squiggle-wavelength-mm" => "8", - "vascular-squiggle-slope" => "0.0", - "squiggle-phase-x-deg" => "90", - "vascular-source-spacing-mm" => "0.5", - "vascular-position-jitter-mm" => "0.05", - "vascular-min-separation-mm" => "0.25", - "vascular-max-sources-per-anchor" => "0", - "vascular-radius-mm" => "1.0", - "network-radius-mm" => "0", - "network-axial-radius-mm" => "10.0", - "network-lateral-y-radius-mm" => "1.5", - "network-lateral-z-radius-mm" => "1.5", - "network-root-count" => "12", - "network-generations" => "3", - "network-branch-length-mm" => "5.0", - "network-branch-step-mm" => "0.4", - "network-branch-angle-deg" => "36", - "network-tortuosity" => "0.18", - "network-orientation" => "isotropic", - "network-density-sigma-mm" => "0", - "network-density-axial-sigma-mm" => "10.0", - "network-density-lateral-y-sigma-mm" => "1.5", - "network-density-lateral-z-sigma-mm" => "1.5", - "network-max-sources-per-center" => "80", - "analysis-mode" => "auto", - "detection-threshold-ratio" => "0.2", - "boundary-threshold-ratios" => "0.5,0.55,0.6,0.65,0.7,0.75", - "auto-threshold-search" => "true", - "auto-threshold-min" => "0.10", - "auto-threshold-max" => "0.95", - "auto-threshold-step" => "0.01", - "peak-method" => "argmax", - "clean-loop-gain" => "0.1", - "clean-max-iter" => "500", - "clean-threshold-ratio" => "0.01", - "from-run-dir" => "", - ) - - provided_keys = Set{String}() - for arg in args - startswith(arg, "--") || error("Unsupported argument format: $arg") - parts = split(arg[3:end], "="; limit=2) - length(parts) == 2 || error("Arguments must use --name=value, got: $arg") - push!(provided_keys, parts[1]) - opts[parts[1]] = parts[2] - end - apply_model_defaults!(opts, provided_keys) - return opts, provided_keys -end - -slug_value(x; digits::Int=1) = replace(string(round(Float64(x); digits=digits)), "-" => "m", "." => "p") -parse_bool(s::AbstractString) = lowercase(strip(s)) in ("1", "true", "yes", "on") - -function parse_dimension(s::AbstractString) - value = strip(s) - value in ("2", "2d", "2D") && return 2 - value in ("3", "3d", "3D") && return 3 - error("--dimension must be 2 or 3, got: $s") -end - -function parse_float_list(spec::AbstractString) - isempty(strip(spec)) && return Float64[] - return [parse(Float64, strip(item)) for item in split(spec, ",") if !isempty(strip(item))] -end - -function parse_int_list(spec::AbstractString) - isempty(strip(spec)) && return Int[] - return [parse(Int, strip(item)) for item in split(spec, ",") if !isempty(strip(item))] -end - -function parse_threshold_ratios(spec::AbstractString) - ratios = parse_float_list(spec) - isempty(ratios) && error("At least one threshold ratio is required.") - all(r -> r > 0, ratios) || error("Threshold ratios must be positive.") - return sort(unique(ratios)) -end - -function parse_threshold_search_ratios(opts) - min_ratio = parse(Float64, opts["auto-threshold-min"]) - max_ratio = parse(Float64, opts["auto-threshold-max"]) - step = parse(Float64, opts["auto-threshold-step"]) - min_ratio > 0 || error("--auto-threshold-min must be positive.") - max_ratio >= min_ratio || error("--auto-threshold-max must be >= --auto-threshold-min.") - step > 0 || error("--auto-threshold-step must be positive.") - n = floor(Int, (max_ratio - min_ratio) / step + 1e-9) - ratios = [round(min_ratio + i * step; digits=6) for i in 0:n] - if isempty(ratios) || ratios[end] < max_ratio - 1e-9 - push!(ratios, round(max_ratio; digits=6)) - end - return sort(unique(ratios)) -end - -function parse_source_model(s::AbstractString) - value = Symbol(lowercase(strip(s))) - value in (:point, :squiggle, :network) || error("--source-model must be point, squiggle, or network, got: $s") - return value -end - -function apply_model_defaults!(opts, provided_keys::Set{String}) +import TranscranialFUS: parse_cli, parse_bool, parse_dimension, parse_float_list, + parse_threshold_ratios, parse_threshold_search_ratios, parse_source_model, parse_aberrator, + parse_simulation_backend, parse_source_phase_mode, parse_source_variability, + source_variability_from_summary, parse_analysis_mode, resolve_reconstruction_mode, + make_window_config, parse_receiver_aperture_mm, parse_point_sources_3d, + parse_squiggle_sources_3d, parse_network_sources_3d, parse_sources, + default_simulation_info, default_recon_frequencies, default_output_dir, + default_reconstruction_output_dir, reject_cached_simulation_options!, json3_to_any, + source_model_from_meta, centerlines_from_emission_meta, detection_truth_mask_from_meta, + save_overview, save_threshold_boundary_detection, save_threshold_boundary_detection_3d, + save_best_threshold_volume_3d, save_napari_npz_3d, compact_window_info, + source_summary, string_key_dict, run_pam_case_3d + +function main(args::AbstractVector{<:AbstractString}=ARGS; dry_run::Bool=false) + dry_run && return TranscranialFUS.run_pam_dry_plan(args) + + opts, provided_keys = parse_cli(String.(args)) dimension = parse_dimension(opts["dimension"]) - if dimension == 3 - !("source-model" in provided_keys) && (opts["source-model"] = "point") - !("sources-mm" in provided_keys) && (opts["sources-mm"] = "30:0:0") - !("anchors-mm" in provided_keys) && (opts["anchors-mm"] = "45:0:0") - !("vascular-squiggle-amplitude-x-mm" in provided_keys) && (opts["vascular-squiggle-amplitude-x-mm"] = "1.0") - !("squiggle-phase-x-deg" in provided_keys) && (opts["squiggle-phase-x-deg"] = "90") - !("frequency-mhz" in provided_keys) && (opts["frequency-mhz"] = "0.5") - !("recon-bandwidth-khz" in provided_keys) && (opts["recon-bandwidth-khz"] = "0") - !("receiver-aperture-mm" in provided_keys) && (opts["receiver-aperture-mm"] = "full") - !("dx-mm" in provided_keys) && (opts["dx-mm"] = "0.2") - !("dy-mm" in provided_keys) && (opts["dy-mm"] = "0.5") - !("dz-mm" in provided_keys) && (opts["dz-mm"] = "0.5") - !("axial-mm" in provided_keys) && (opts["axial-mm"] = "60") - !("transverse-mm" in provided_keys) && (opts["transverse-mm"] = "32") - !("dt-ns" in provided_keys) && (opts["dt-ns"] = "80") - !("t-max-us" in provided_keys) && (opts["t-max-us"] = "60") - !("zero-pad-factor" in provided_keys) && (opts["zero-pad-factor"] = "4") - !("num-cycles" in provided_keys) && (opts["num-cycles"] = "5") - !("phase-mode" in provided_keys) && (opts["phase-mode"] = "coherent") - !("recon-step-um" in provided_keys) && (opts["recon-step-um"] = "50") - !("use-gpu" in provided_keys) && (opts["use-gpu"] = "true") - end source_model = parse_source_model(opts["source-model"]) - if dimension == 3 && source_model in (:squiggle, :network) - !("vascular-source-spacing-mm" in provided_keys) && (opts["vascular-source-spacing-mm"] = "0.5") - !("vascular-min-separation-mm" in provided_keys) && (opts["vascular-min-separation-mm"] = "0.25") - !("recon-bandwidth-khz" in provided_keys) && (opts["recon-bandwidth-khz"] = "40") - !("recon-window-us" in provided_keys) && (opts["recon-window-us"] = "40") - !("recon-hop-us" in provided_keys) && (opts["recon-hop-us"] = "20") - !("boundary-threshold-ratios" in provided_keys) && (opts["boundary-threshold-ratios"] = "0.5,0.55,0.6,0.65,0.7,0.75") - end - if source_model == :point - !("source-phase-mode" in provided_keys) && (opts["source-phase-mode"] = "coherent") - !("recon-bandwidth-khz" in provided_keys) && (opts["recon-bandwidth-khz"] = "0") - !("receiver-aperture-mm" in provided_keys) && (opts["receiver-aperture-mm"] = "50") - !("transverse-mm" in provided_keys) && (opts["transverse-mm"] = "60") - !("dt-ns" in provided_keys) && (opts["dt-ns"] = "40") - !("t-max-us" in provided_keys) && (opts["t-max-us"] = "60") - !("axial-mm" in provided_keys) && (opts["axial-mm"] = "60") - !("phase-mode" in provided_keys) && (opts["phase-mode"] = "coherent") - end - return opts -end - -function parse_aberrator(s::AbstractString) - value = Symbol(lowercase(strip(s))) - value in (:none, :water, :lens, :skull) || error("Unknown aberrator: $s") - return value -end + from_run_dir = strip(opts["from-run-dir"]) + detection_truth_radius_m = parse(Float64, opts["vascular-radius-mm"]) * 1e-3 + detection_threshold_ratio = parse(Float64, opts["detection-threshold-ratio"]) + boundary_threshold_ratios = parse_threshold_ratios(opts["boundary-threshold-ratios"]) + auto_threshold_search = parse_bool(opts["auto-threshold-search"]) + threshold_score_ratios = auto_threshold_search ? parse_threshold_search_ratios(opts) : boundary_threshold_ratios -function parse_sim_mode(s::AbstractString) - value = Symbol(lowercase(strip(s))) - value in (:auto, :analytic, :kwave) || error("Unknown --sim-mode: $s (must be auto, analytic, or kwave)") - value == :auto && return :kwave - return value -end - -function parse_cavitation_model(s::AbstractString) - value = Symbol(replace(lowercase(strip(s)), "-" => "_")) - value in (:harmonic_cos, :gaussian_pulse) || - error("--cavitation-model must be harmonic-cos or gaussian-pulse, got: $s") - return value -end - -function parse_source_phase_mode(s::AbstractString) - value = Symbol(replace(lowercase(strip(s)), "-" => "_")) - value in (:coherent, :random_static_phase, :random_phase_per_window, :random_phase_per_realization) || - error("--source-phase-mode must be coherent, random_static_phase, random_phase_per_window, or random_phase_per_realization, got: $s") - return value -end - -parse_source_variability(opts) = SourceVariabilityConfig( - frequency_jitter_fraction=parse(Float64, opts["frequency-jitter-percent"]) / 100.0, -) - -function source_variability_from_summary(summary) - if isnothing(summary) || !hasproperty(summary, :source_variability) - return SourceVariabilityConfig() - end - sv = summary.source_variability - if hasproperty(sv, :frequency_jitter_percent) - return SourceVariabilityConfig(frequency_jitter_fraction=Float64(sv.frequency_jitter_percent) / 100.0) - end - return SourceVariabilityConfig() -end - -function parse_analysis_mode(s::AbstractString, source_model::Symbol) - value = Symbol(lowercase(strip(s))) - value == :auto && return source_model in (:squiggle, :network) ? :detection : :localization - value in (:localization, :detection) || error("--analysis-mode must be auto, localization, or detection, got: $s") - return value -end - -resolve_reconstruction_mode(s::AbstractString, source_model::Symbol) = - TranscranialFUS.pam_reconstruction_mode(s, source_model) - -function parse_window_taper(s::AbstractString) - value = Symbol(replace(lowercase(strip(s)), "-" => "_")) - value in (:hann, :none, :rect, :rectangular, :tukey) || - error("--recon-window-taper must be hann, none, rectangular, or tukey, got: $s") - return value -end - -function make_window_config(opts, reconstruction_mode::Symbol) - return PAMWindowConfig( - enabled=reconstruction_mode == :windowed, - window_duration=parse(Float64, opts["recon-window-us"]) * 1e-6, - hop=parse(Float64, opts["recon-hop-us"]) * 1e-6, - taper=parse_window_taper(opts["recon-window-taper"]), - min_energy_ratio=parse(Float64, opts["recon-min-window-energy-ratio"]), - accumulation=:intensity, - ) -end - -function parse_receiver_aperture_mm(s::AbstractString) - value = lowercase(strip(s)) - value in ("none", "full", "all") && return nothing - return parse(Float64, value) * 1e-3 -end - -function parse_transducer_mm(s::AbstractString) - parts = split(strip(s), ":"; limit=2) - length(parts) == 2 || error("--transducer-mm must be depth_mm:lateral_mm, got: $s") - return parse(Float64, strip(parts[1])) * 1e-3, parse(Float64, strip(parts[2])) * 1e-3 -end - -point_pairs_m(points) = [[point[1], point[2]] for point in points] -centerlines_m(centerlines) = [point_pairs_m(line) for line in centerlines] - -function expand_source_values(values::Vector{Float64}, n::Int, default::Float64) - isempty(values) && return fill(default, n) - length(values) == 1 && return fill(values[1], n) - length(values) == n && return values - error("Per-source parameter list must have length 1 or match the number of sources ($n).") -end - -function parse_coordinate_pairs_mm(spec::AbstractString, option_name::AbstractString) - coord_tokens = [strip(token) for token in split(spec, ",") if !isempty(strip(token))] - 1 <= length(coord_tokens) <= 20 || error("Provide between 1 and 20 coordinates via --$option_name=depth:lateral,...") - pairs = Tuple{Float64, Float64}[] - for token in coord_tokens - parts = split(token, ":"; limit=2) - length(parts) == 2 || error("Each coordinate must be depth_mm:lateral_mm, got: $token") - push!(pairs, (parse(Float64, strip(parts[1])) * 1e-3, parse(Float64, strip(parts[2])) * 1e-3)) - end - return pairs -end - -function parse_coordinate_triples_mm(spec::AbstractString, option_name::AbstractString) - coord_tokens = [strip(token) for token in split(spec, ",") if !isempty(strip(token))] - 1 <= length(coord_tokens) <= 20 || error("Provide between 1 and 20 coordinates via --$option_name=depth:y:z,...") - triples = NTuple{3, Float64}[] - for token in coord_tokens - parts = split(token, ":"; limit=3) - length(parts) == 3 || error("3D coordinates must be depth:y:z in mm, got: $token") - push!(triples, ( - parse(Float64, strip(parts[1])) * 1e-3, - parse(Float64, strip(parts[2])) * 1e-3, - parse(Float64, strip(parts[3])) * 1e-3, - )) - end - return triples -end - -function parse_point_sources(opts) - coordinates = parse_coordinate_pairs_mm(opts["sources-mm"], "sources-mm") - n_sources = length(coordinates) - frequencies_mhz = expand_source_values( - parse_float_list(opts["source-frequencies-mhz"]), - n_sources, - parse(Float64, opts["frequency-mhz"]), - ) - amplitudes_pa = expand_source_values( - parse_float_list(opts["source-amplitudes-pa"]), - n_sources, - parse(Float64, opts["amplitude-pa"]), - ) - phases_deg = expand_source_values(parse_float_list(opts["phases-deg"]), n_sources, 0.0) - delays_us = expand_source_values(parse_float_list(opts["delays-us"]), n_sources, 0.0) - num_cycles = parse(Int, opts["num-cycles"]) - - phase_mode = lowercase(strip(opts["phase-mode"])) - phase_mode in ("coherent", "random", "jittered") || - error("Point --phase-mode must be coherent, random, or jittered, got: $phase_mode") - rng = Random.MersenneTwister(parse(Int, opts["random-seed"])) - if phase_mode == "random" - phases_deg = rand(rng, n_sources) .* 360.0 - elseif phase_mode == "jittered" - phases_deg = phases_deg .+ randn(rng, n_sources) .* (parse(Float64, opts["phase-jitter-rad"]) * 180 / pi) - end - - sources = EmissionSource2D[] - for (idx, (depth_m, lateral_m)) in pairs(coordinates) - push!(sources, PointSource2D( - depth=depth_m, - lateral=lateral_m, - frequency=frequencies_mhz[idx] * 1e6, - amplitude=amplitudes_pa[idx], - phase=phases_deg[idx] * pi / 180, - delay=delays_us[idx] * 1e-6, - num_cycles=num_cycles, - )) - end - return sources, Dict{String, Any}( - "source_model" => "point", - "coordinates_m" => [collect(coord) for coord in coordinates], - "n_coordinate_sources" => n_sources, - "n_emission_sources" => length(sources), - "physical_source_count" => length(sources), - "emission_event_count" => length(sources), - "activity_model" => Dict("activity_mode" => "point_tone_burst"), - "phase_mode" => phase_mode, - "frequencies_hz" => frequencies_mhz .* 1e6, - "amplitudes_pa" => amplitudes_pa, - "phases_rad" => phases_deg .* pi ./ 180, - "delays_s" => delays_us .* 1e-6, - "num_cycles" => num_cycles, - "random_seed" => parse(Int, opts["random-seed"]), - ) -end - -function parse_point_sources_3d(opts) - coordinates = parse_coordinate_triples_mm(opts["sources-mm"], "sources-mm") - n_sources = length(coordinates) - frequencies_mhz = expand_source_values( - parse_float_list(opts["source-frequencies-mhz"]), - n_sources, - parse(Float64, opts["frequency-mhz"]), - ) - amplitudes_pa = expand_source_values( - parse_float_list(opts["source-amplitudes-pa"]), - n_sources, - parse(Float64, opts["amplitude-pa"]), - ) - phases_deg = expand_source_values(parse_float_list(opts["phases-deg"]), n_sources, 0.0) - delays_us = expand_source_values(parse_float_list(opts["delays-us"]), n_sources, 0.0) - num_cycles = parse(Int, opts["num-cycles"]) - - phase_mode = lowercase(strip(opts["phase-mode"])) - phase_mode in ("coherent", "random", "jittered") || - error("Point --phase-mode must be coherent, random, or jittered, got: $phase_mode") - rng = Random.MersenneTwister(parse(Int, opts["random-seed"])) - if phase_mode == "random" - phases_deg = rand(rng, n_sources) .* 360.0 - elseif phase_mode == "jittered" - phases_deg = phases_deg .+ randn(rng, n_sources) .* (parse(Float64, opts["phase-jitter-rad"]) * 180 / pi) - end - - sources = EmissionSource3D[] - for (idx, (depth_m, lateral_y_m, lateral_z_m)) in pairs(coordinates) - push!(sources, PointSource3D( - depth=depth_m, - lateral_y=lateral_y_m, - lateral_z=lateral_z_m, - frequency=frequencies_mhz[idx] * 1e6, - amplitude=amplitudes_pa[idx], - phase=phases_deg[idx] * pi / 180, - delay=delays_us[idx] * 1e-6, - num_cycles=num_cycles, - )) - end - return sources, Dict{String, Any}( - "source_model" => "point3d", - "coordinates_m" => [collect(coord) for coord in coordinates], - "n_coordinate_sources" => n_sources, - "n_emission_sources" => length(sources), - "physical_source_count" => length(sources), - "emission_event_count" => length(sources), - "activity_model" => Dict("activity_mode" => "point_tone_burst_3d"), - "phase_mode" => phase_mode, - "frequencies_hz" => frequencies_mhz .* 1e6, - "amplitudes_pa" => amplitudes_pa, - "phases_rad" => phases_deg .* pi ./ 180, - "delays_s" => delays_us .* 1e-6, - "num_cycles" => num_cycles, - "random_seed" => parse(Int, opts["random-seed"]), - ) -end - -centerlines_m_3d(centerlines) = [[[p[1], p[2], p[3]] for p in line] for line in centerlines] - -function parse_squiggle_sources_3d(opts, cfg::PAMConfig3D) - anchors = parse_coordinate_triples_mm(opts["anchors-mm"], "anchors-mm") - f0 = parse(Float64, opts["fundamental-mhz"]) * 1e6 - harmonics = parse_int_list(opts["harmonics"]) - isempty(harmonics) && error("--harmonics must be a non-empty integer list.") - harmonic_amplitudes = parse_float_list(opts["harmonic-amplitudes"]) - length(harmonic_amplitudes) == length(harmonics) || - error("--harmonic-amplitudes must have the same length as --harmonics ($(length(harmonics))).") - - n_anchors = length(anchors) - n_bubbles_per = expand_source_values(parse_float_list(opts["n-bubbles"]), n_anchors, 10.0) - delays_us = expand_source_values(parse_float_list(opts["delays-us"]), n_anchors, 0.0) - max_sources_raw = parse(Int, opts["vascular-max-sources-per-anchor"]) - max_sources = max_sources_raw <= 0 ? nothing : max_sources_raw - phase_mode = Symbol(replace(lowercase(strip(opts["phase-mode"])), "-" => "_")) - - sources = EmissionSource3D[] - all_centerlines_m = Any[] - anchors_meta = Dict{String, Any}[] - rng = Random.MersenneTwister(parse(Int, opts["random-seed"])) - half_y = cfg.transverse_dim_y / 2 - half_z = cfg.transverse_dim_z / 2 - - for (idx, anchor) in pairs(anchors) - anchor_sources, anchor_meta = make_squiggle_bubble_sources_3d( - [anchor]; - root_length = parse(Float64, opts["vascular-length-mm"]) * 1e-3, - squiggle_amplitude_y = parse(Float64, opts["vascular-squiggle-amplitude-mm"]) * 1e-3, - squiggle_amplitude_x = parse(Float64, opts["vascular-squiggle-amplitude-x-mm"]) * 1e-3, - squiggle_wavelength = parse(Float64, opts["vascular-squiggle-wavelength-mm"]) * 1e-3, - squiggle_phase_x = parse(Float64, opts["squiggle-phase-x-deg"]) * pi / 180, - squiggle_slope_x = parse(Float64, opts["vascular-squiggle-slope"]), - squiggle_slope_y = 0.0, - source_spacing = parse(Float64, opts["vascular-source-spacing-mm"]) * 1e-3, - position_jitter = parse(Float64, opts["vascular-position-jitter-mm"]) * 1e-3, - min_separation = parse(Float64, opts["vascular-min-separation-mm"]) * 1e-3, - max_sources_per_anchor = max_sources, - depth_bounds = (0.0, Inf), - lateral_y_bounds = (-half_y, half_y), - lateral_z_bounds = (-half_z, half_z), - fundamental = f0, - amplitude = parse(Float64, opts["amplitude-pa"]), - n_bubbles = n_bubbles_per[idx], - harmonics = harmonics, - harmonic_amplitudes = harmonic_amplitudes, - gate_duration = parse(Float64, opts["gate-us"]) * 1e-6, - taper_ratio = parse(Float64, opts["taper-ratio"]), - delay = delays_us[idx] * 1e-6, - phase_mode = phase_mode, - phase_jitter = parse(Float64, opts["phase-jitter-rad"]), - transducer_depth = -parse(Float64, opts["skull-transducer-distance-mm"]) * 1e-3, - transducer_y = 0.0, - transducer_z = 0.0, - c0 = cfg.c0, - rng = rng, - ) - append!(sources, anchor_sources) - anchor_cls = centerlines_m_3d(anchor_meta[:centerlines]) - append!(all_centerlines_m, anchor_cls) - push!(anchors_meta, Dict( - "anchor_m" => collect(anchor), - "source_count" => length(anchor_sources), - "centerlines_m" => anchor_cls, - )) - end - - return sources, Dict{String, Any}( - "source_model" => "squiggle3d", - "anchor_clusters_m" => [collect(anchor) for anchor in anchors], - "n_anchor_clusters" => length(anchors), - "n_emission_sources" => length(sources), - "physical_source_count" => length(sources), - "emission_event_count" => length(sources), - "activity_model" => Dict("activity_mode" => "random_phase_per_window"), - "phase_mode" => String(phase_mode), - "fundamental_hz" => f0, - "harmonics" => harmonics, - "harmonic_amplitudes" => harmonic_amplitudes, - "gate_duration_s" => parse(Float64, opts["gate-us"]) * 1e-6, - "phase_jitter_rad" => parse(Float64, opts["phase-jitter-rad"]), - "random_seed" => parse(Int, opts["random-seed"]), - "n_bubbles_per_cluster" => n_bubbles_per, - "delays_s" => delays_us .* 1e-6, - "squiggle" => Dict( - "length_m" => parse(Float64, opts["vascular-length-mm"]) * 1e-3, - "squiggle_amplitude_y_m" => parse(Float64, opts["vascular-squiggle-amplitude-mm"]) * 1e-3, - "squiggle_amplitude_x_m" => parse(Float64, opts["vascular-squiggle-amplitude-x-mm"]) * 1e-3, - "squiggle_wavelength_m" => parse(Float64, opts["vascular-squiggle-wavelength-mm"]) * 1e-3, - "squiggle_phase_x_deg" => parse(Float64, opts["squiggle-phase-x-deg"]), - "squiggle_slope" => parse(Float64, opts["vascular-squiggle-slope"]), - "source_spacing_m" => parse(Float64, opts["vascular-source-spacing-mm"]) * 1e-3, - "position_jitter_m" => parse(Float64, opts["vascular-position-jitter-mm"]) * 1e-3, - "min_separation_m" => parse(Float64, opts["vascular-min-separation-mm"]) * 1e-3, - "max_sources_per_anchor" => max_sources_raw, - "truth_radius_m" => parse(Float64, opts["vascular-radius-mm"]) * 1e-3, - "centerlines_m" => all_centerlines_m, - "anchors" => anchors_meta, - ), - ) -end - -function parse_network_sources_3d(opts, cfg::PAMConfig3D) - centers = parse_coordinate_triples_mm(opts["anchors-mm"], "anchors-mm") - f0 = parse(Float64, opts["fundamental-mhz"]) * 1e6 - harmonics = parse_int_list(opts["harmonics"]) - isempty(harmonics) && error("--harmonics must be a non-empty integer list.") - harmonic_amplitudes = parse_float_list(opts["harmonic-amplitudes"]) - length(harmonic_amplitudes) == length(harmonics) || - error("--harmonic-amplitudes must have the same length as --harmonics ($(length(harmonics))).") - - n_centers = length(centers) - n_bubbles_per = expand_source_values(parse_float_list(opts["n-bubbles"]), n_centers, 1.0) - delays_us = expand_source_values(parse_float_list(opts["delays-us"]), n_centers, 0.0) - max_sources_raw = parse(Int, opts["network-max-sources-per-center"]) - max_sources = max_sources_raw <= 0 ? nothing : max_sources_raw - phase_mode = Symbol(replace(lowercase(strip(opts["phase-mode"])), "-" => "_")) - network_radius_m = parse(Float64, opts["network-radius-mm"]) * 1e-3 - axial_radius_m = parse(Float64, opts["network-axial-radius-mm"]) * 1e-3 - lateral_y_radius_m = parse(Float64, opts["network-lateral-y-radius-mm"]) * 1e-3 - lateral_z_radius_m = parse(Float64, opts["network-lateral-z-radius-mm"]) * 1e-3 - ellipsoid_radii_m = network_radius_m > 0 ? - [network_radius_m, network_radius_m, network_radius_m] : - [axial_radius_m, lateral_y_radius_m, lateral_z_radius_m] - density_sigma_m = parse(Float64, opts["network-density-sigma-mm"]) * 1e-3 - density_axial_sigma_m = parse(Float64, opts["network-density-axial-sigma-mm"]) * 1e-3 - density_lateral_y_sigma_m = parse(Float64, opts["network-density-lateral-y-sigma-mm"]) * 1e-3 - density_lateral_z_sigma_m = parse(Float64, opts["network-density-lateral-z-sigma-mm"]) * 1e-3 - density_sigmas_m = density_sigma_m > 0 ? - [density_sigma_m, density_sigma_m, density_sigma_m] : - [density_axial_sigma_m, density_lateral_y_sigma_m, density_lateral_z_sigma_m] - - sources = EmissionSource3D[] - all_centerlines_m = Any[] - centers_meta = Dict{String, Any}[] - rng = Random.MersenneTwister(parse(Int, opts["random-seed"])) - half_y = cfg.transverse_dim_y / 2 - half_z = cfg.transverse_dim_z / 2 - - for (idx, center) in pairs(centers) - center_sources, network_meta = make_network_bubble_sources_3d( - [center]; - sphere_radius = network_radius_m, - axial_radius = axial_radius_m, - lateral_y_radius = lateral_y_radius_m, - lateral_z_radius = lateral_z_radius_m, - root_count = parse(Int, opts["network-root-count"]), - generations = parse(Int, opts["network-generations"]), - branch_length = parse(Float64, opts["network-branch-length-mm"]) * 1e-3, - branch_step = parse(Float64, opts["network-branch-step-mm"]) * 1e-3, - branch_angle = parse(Float64, opts["network-branch-angle-deg"]) * pi / 180, - tortuosity = parse(Float64, opts["network-tortuosity"]), - network_orientation = Symbol(replace(lowercase(strip(opts["network-orientation"])), "-" => "_")), - source_spacing = parse(Float64, opts["vascular-source-spacing-mm"]) * 1e-3, - density_sigma = density_sigma_m, - density_sigma_depth = density_axial_sigma_m, - density_sigma_y = density_lateral_y_sigma_m, - density_sigma_z = density_lateral_z_sigma_m, - min_separation = parse(Float64, opts["vascular-min-separation-mm"]) * 1e-3, - max_sources_per_center = max_sources, - depth_bounds = (0.0, Inf), - lateral_y_bounds = (-half_y, half_y), - lateral_z_bounds = (-half_z, half_z), - fundamental = f0, - amplitude = parse(Float64, opts["amplitude-pa"]), - n_bubbles = n_bubbles_per[idx], - harmonics = harmonics, - harmonic_amplitudes = harmonic_amplitudes, - gate_duration = parse(Float64, opts["gate-us"]) * 1e-6, - taper_ratio = parse(Float64, opts["taper-ratio"]), - delay = delays_us[idx] * 1e-6, - phase_mode = phase_mode, - phase_jitter = parse(Float64, opts["phase-jitter-rad"]), - transducer_depth = -parse(Float64, opts["skull-transducer-distance-mm"]) * 1e-3, - transducer_y = 0.0, - transducer_z = 0.0, - c0 = cfg.c0, - rng = rng, + if dimension == 3 + isempty(from_run_dir) || error("--from-run-dir is not implemented for 3D PAM yet.") + source_model in (:point, :squiggle, :network) || + error("3D PAM CLI supports --source-model=point, --source-model=squiggle, or --source-model=network.") + aberrator = parse_aberrator(opts["aberrator"]) + aberrator in (:none, :skull) || error("3D PAM CLI currently supports only --aberrator=none or --aberrator=skull.") + + dy_mm = isempty(strip(opts["dy-mm"])) ? parse(Float64, opts["dz-mm"]) : parse(Float64, opts["dy-mm"]) + transverse_y_mm = isempty(strip(opts["transverse-y-mm"])) ? parse(Float64, opts["transverse-mm"]) : parse(Float64, opts["transverse-y-mm"]) + transverse_z_mm = isempty(strip(opts["transverse-z-mm"])) ? parse(Float64, opts["transverse-mm"]) : parse(Float64, opts["transverse-z-mm"]) + receiver_aperture_y_spec = isempty(strip(opts["receiver-aperture-y-mm"])) ? opts["receiver-aperture-mm"] : opts["receiver-aperture-y-mm"] + receiver_aperture_z_spec = isempty(strip(opts["receiver-aperture-z-mm"])) ? opts["receiver-aperture-mm"] : opts["receiver-aperture-z-mm"] + + cfg_base = PAMConfig3D( + dx=parse(Float64, opts["dx-mm"]) * 1e-3, + dy=dy_mm * 1e-3, + dz=parse(Float64, opts["dz-mm"]) * 1e-3, + axial_dim=parse(Float64, opts["axial-mm"]) * 1e-3, + transverse_dim_y=transverse_y_mm * 1e-3, + transverse_dim_z=transverse_z_mm * 1e-3, + t_max=parse(Float64, opts["t-max-us"]) * 1e-6, + dt=parse(Float64, opts["dt-ns"]) * 1e-9, + zero_pad_factor=parse(Int, opts["zero-pad-factor"]), + receiver_aperture_y=parse_receiver_aperture_mm(receiver_aperture_y_spec), + receiver_aperture_z=parse_receiver_aperture_mm(receiver_aperture_z_spec), + peak_suppression_radius=parse(Float64, opts["peak-suppression-radius-mm"]) * 1e-3, + success_tolerance=parse(Float64, opts["success-tolerance-mm"]) * 1e-3, + axial_gain_power=parse(Float64, opts["axial-gain-power"]), ) - append!(sources, center_sources) - center_cls = centerlines_m_3d(network_meta[:centerlines]) - append!(all_centerlines_m, center_cls) - push!(centers_meta, Dict( - "center_m" => collect(center), - "source_count" => length(center_sources), - "centerline_count" => length(network_meta[:centerlines]), - "centerlines_m" => center_cls, - )) - end - return sources, Dict{String, Any}( - "source_model" => "network3d", - "network_centers_m" => [collect(center) for center in centers], - "n_network_centers" => length(centers), - "n_emission_sources" => length(sources), - "physical_source_count" => length(sources), - "emission_event_count" => length(sources), - "activity_model" => Dict("activity_mode" => "random_phase_per_window"), - "phase_mode" => String(phase_mode), - "fundamental_hz" => f0, - "harmonics" => harmonics, - "harmonic_amplitudes" => harmonic_amplitudes, - "gate_duration_s" => parse(Float64, opts["gate-us"]) * 1e-6, - "phase_jitter_rad" => parse(Float64, opts["phase-jitter-rad"]), - "random_seed" => parse(Int, opts["random-seed"]), - "n_bubbles_per_cluster" => n_bubbles_per, - "delays_s" => delays_us .* 1e-6, - "network" => Dict( - "radius_m" => network_radius_m, - "axial_radius_m" => axial_radius_m, - "lateral_y_radius_m" => lateral_y_radius_m, - "lateral_z_radius_m" => lateral_z_radius_m, - "ellipsoid_radii_m" => ellipsoid_radii_m, - "root_count" => parse(Int, opts["network-root-count"]), - "generations" => parse(Int, opts["network-generations"]), - "branch_length_m" => parse(Float64, opts["network-branch-length-mm"]) * 1e-3, - "branch_step_m" => parse(Float64, opts["network-branch-step-mm"]) * 1e-3, - "branch_angle_deg" => parse(Float64, opts["network-branch-angle-deg"]), - "tortuosity" => parse(Float64, opts["network-tortuosity"]), - "orientation" => opts["network-orientation"], - "density_sigma_m" => density_sigma_m, - "density_sigmas_m" => density_sigmas_m, - "source_spacing_m" => parse(Float64, opts["vascular-source-spacing-mm"]) * 1e-3, - "min_separation_m" => parse(Float64, opts["vascular-min-separation-mm"]) * 1e-3, - "max_sources_per_center" => max_sources_raw, - "truth_radius_m" => parse(Float64, opts["vascular-radius-mm"]) * 1e-3, - "centerlines_m" => all_centerlines_m, - "centers" => centers_meta, - ), - ) -end - -function parse_squiggle_sources(opts, cfg::PAMConfig) - anchors = parse_coordinate_pairs_mm(opts["anchors-mm"], "anchors-mm") - f0 = parse(Float64, opts["fundamental-mhz"]) * 1e6 - harmonics = parse_int_list(opts["harmonics"]) - isempty(harmonics) && error("--harmonics must be a non-empty integer list.") - harmonic_amplitudes = parse_float_list(opts["harmonic-amplitudes"]) - length(harmonic_amplitudes) == length(harmonics) || - error("--harmonic-amplitudes must have the same length as --harmonics ($(length(harmonics))).") - - n_anchors = length(anchors) - n_bubbles_per = expand_source_values(parse_float_list(opts["n-bubbles"]), n_anchors, 10.0) - delays_us = expand_source_values(parse_float_list(opts["delays-us"]), n_anchors, 0.0) - tx_depth, tx_lateral = parse_transducer_mm(opts["transducer-mm"]) - max_sources_raw = parse(Int, opts["vascular-max-sources-per-anchor"]) - max_sources = max_sources_raw <= 0 ? nothing : max_sources_raw - phase_mode = Symbol(replace(lowercase(strip(opts["phase-mode"])), "-" => "_")) + sources, emission_meta = if source_model == :point + parse_point_sources_3d(opts) + elseif source_model == :network + parse_network_sources_3d(opts, cfg_base) + else + parse_squiggle_sources_3d(opts, cfg_base) + end + bottom_margin_m = parse(Float64, opts["bottom-margin-mm"]) * 1e-3 + cfg = fit_pam_config_3d(cfg_base, sources; min_bottom_margin=bottom_margin_m) - sources = EmissionSource2D[] - all_centerlines_m = Any[] - anchors_meta = Dict{String, Any}[] - rng = Random.MersenneTwister(parse(Int, opts["random-seed"])) - for (idx, anchor) in pairs(anchors) - anchor_sources, anchor_meta = make_squiggle_bubble_sources( - [anchor]; - root_length=parse(Float64, opts["vascular-length-mm"]) * 1e-3, - squiggle_amplitude=parse(Float64, opts["vascular-squiggle-amplitude-mm"]) * 1e-3, - squiggle_wavelength=parse(Float64, opts["vascular-squiggle-wavelength-mm"]) * 1e-3, - squiggle_slope=parse(Float64, opts["vascular-squiggle-slope"]), - source_spacing=parse(Float64, opts["vascular-source-spacing-mm"]) * 1e-3, - position_jitter=parse(Float64, opts["vascular-position-jitter-mm"]) * 1e-3, - min_separation=parse(Float64, opts["vascular-min-separation-mm"]) * 1e-3, - max_sources_per_anchor=max_sources, - depth_bounds=(0.0, Inf), - lateral_bounds=(-cfg.transverse_dim / 2, cfg.transverse_dim / 2), - fundamental=f0, - amplitude=parse(Float64, opts["amplitude-pa"]), - n_bubbles=n_bubbles_per[idx], - harmonics=harmonics, - harmonic_amplitudes=harmonic_amplitudes, - cavitation_model=parse_cavitation_model(opts["cavitation-model"]), - gate_duration=parse(Float64, opts["gate-us"]) * 1e-6, - taper_ratio=parse(Float64, opts["taper-ratio"]), - delay=delays_us[idx] * 1e-6, - phase_mode=phase_mode, - phase_jitter=parse(Float64, opts["phase-jitter-rad"]), - transducer_depth=tx_depth, - transducer_lateral=tx_lateral, - c0=cfg.c0, - rng=rng, + out_dir = if haskey(opts, "out-dir") && !isempty(strip(opts["out-dir"])) + opts["out-dir"] + else + default_output_dir(opts, sources, cfg, emission_meta) + end + mkpath(out_dir) + + c, rho, medium_info = make_pam_medium_3d(cfg; + aberrator = aberrator, + ct_path = opts["ct-path"], + slice_index_z = parse(Int, opts["slice-index"]), + skull_to_transducer = parse(Float64, opts["skull-transducer-distance-mm"]) * 1e-3, + hu_bone_thr = parse(Int, opts["hu-bone-thr"]), ) - append!(sources, anchor_sources) - anchor_centerlines = centerlines_m(anchor_meta[:centerlines]) - append!(all_centerlines_m, anchor_centerlines) - push!(anchors_meta, Dict( - "anchor_m" => collect(anchor), - "source_count" => length(anchor_sources), - "centerlines_m" => anchor_centerlines, - )) - end - - return sources, Dict{String, Any}( - "source_model" => "squiggle", - "anchor_clusters_m" => [collect(anchor) for anchor in anchors], - "n_anchor_clusters" => length(anchors), - "n_emission_sources" => length(sources), - "physical_source_count" => length(sources), - "emission_event_count" => length(sources), - "activity_model" => Dict("activity_mode" => "random_phase_per_window"), - "phase_mode" => String(phase_mode), - "fundamental_hz" => f0, - "harmonics" => harmonics, - "harmonic_amplitudes" => harmonic_amplitudes, - "cavitation_model" => opts["cavitation-model"], - "gate_duration_s" => parse(Float64, opts["gate-us"]) * 1e-6, - "transducer_m" => [tx_depth, tx_lateral], - "phase_jitter_rad" => parse(Float64, opts["phase-jitter-rad"]), - "random_seed" => parse(Int, opts["random-seed"]), - "n_bubbles_per_cluster" => n_bubbles_per, - "delays_s" => delays_us .* 1e-6, - "squiggle" => Dict( - "length_m" => parse(Float64, opts["vascular-length-mm"]) * 1e-3, - "squiggle_amplitude_m" => parse(Float64, opts["vascular-squiggle-amplitude-mm"]) * 1e-3, - "squiggle_wavelength_m" => parse(Float64, opts["vascular-squiggle-wavelength-mm"]) * 1e-3, - "squiggle_slope" => parse(Float64, opts["vascular-squiggle-slope"]), - "source_spacing_m" => parse(Float64, opts["vascular-source-spacing-mm"]) * 1e-3, - "position_jitter_m" => parse(Float64, opts["vascular-position-jitter-mm"]) * 1e-3, - "min_separation_m" => parse(Float64, opts["vascular-min-separation-mm"]) * 1e-3, - "max_sources_per_anchor" => max_sources_raw, - "truth_radius_m" => parse(Float64, opts["vascular-radius-mm"]) * 1e-3, - "centerlines_m" => all_centerlines_m, - "anchors" => anchors_meta, - ), - ) -end - -function parse_sources(opts, cfg::PAMConfig) - source_model = parse_source_model(opts["source-model"]) - source_model == :point && return parse_point_sources(opts) - source_model == :squiggle && return parse_squiggle_sources(opts, cfg) - error("2D PAM CLI supports --source-model=point or --source-model=squiggle.") -end - -function default_output_dir(opts, sources, cfg, emission_meta) - timestamp = Dates.format(Dates.now(), "yyyymmdd_HHMMSS") - source_model = lowercase(String(emission_meta["source_model"])) - lateral_slug = if cfg isa PAMConfig3D - "laty$(slug_value(cfg.transverse_dim_y * 1e3; digits=0))mm_latz$(slug_value(cfg.transverse_dim_z * 1e3; digits=0))mm" - else - "lat$(slug_value(cfg.transverse_dim * 1e3; digits=0))mm" - end - parts = String[ - timestamp, - "run_pam", - cfg isa PAMConfig3D ? "3d" : "2d", - lowercase(opts["aberrator"]), - source_model, - "$(length(sources))src", - "ax$(slug_value(cfg.axial_dim * 1e3; digits=0))mm", - lateral_slug, - ] - if occursin("squiggle", source_model) || occursin("network", source_model) - count_key = haskey(emission_meta, "n_anchor_clusters") ? "n_anchor_clusters" : "n_network_centers" - label = occursin("network", source_model) ? "centers" : "anchors" - insert!(parts, 5, "$(emission_meta[count_key])$(label)") - push!(parts, "f$(slug_value(parse(Float64, opts["fundamental-mhz"]); digits=2))mhz") - push!(parts, "h$(replace(opts["harmonics"], "," => ""))") - push!(parts, replace(lowercase(opts["source-phase-mode"]), "_" => "")) - else - push!(parts, "f$(slug_value(parse(Float64, opts["frequency-mhz"]); digits=2))mhz") - end - if lowercase(opts["aberrator"]) == "skull" - insert!(parts, length(parts), "slice" * opts["slice-index"]) - insert!(parts, length(parts), "st$(slug_value(parse(Float64, opts["skull-transducer-distance-mm"]); digits=1))mm") - end - return joinpath(pwd(), "outputs", join(parts, "_")) -end - -function default_reconstruction_output_dir(source_dir::AbstractString) - timestamp = Dates.format(Dates.now(), "yyyymmdd_HHMMSS") - source_name = basename(normpath(source_dir)) - return joinpath(pwd(), "outputs", "$(timestamp)_reconstruct_$(source_name)") -end - -function reject_cached_simulation_options!(provided_keys::Set{String}, blocked_keys) - illegal = sort(collect(intersect(provided_keys, Set(blocked_keys)))) - isempty(illegal) && return nothing - formatted = join(["--$key" for key in illegal], ", ") - error("--from-run-dir reuses the previous RF simulation, medium, sources, and grid. Remove simulation-specific option(s): $formatted") -end - -function default_simulation_info(cfg::PAMConfig) - return Dict{Symbol, Any}( - :receiver_row => receiver_row(cfg), - :receiver_cols => receiver_col_range(cfg), - :source_indices => Tuple{Int, Int}[], - ) -end - -function default_simulation_info(cfg::PAMConfig3D) - return Dict{Symbol, Any}( - :receiver_row => receiver_row(cfg), - :receiver_cols_y => receiver_col_range_y(cfg), - :receiver_cols_z => receiver_col_range_z(cfg), - :source_indices => NTuple{3, Int}[], - ) -end - -function default_recon_frequencies(sources) - freqs = Float64[] - for src in sources - append!(freqs, emission_frequencies(src)) - end - return sort(unique(freqs)) -end - -function _sample_source_signal(signal::AbstractVector{<:Real}, t::Real, dt::Real) - u = Float64(t) / Float64(dt) + 1.0 - i0 = floor(Int, u) - i0 < 1 && return 0.0 - i0 > length(signal) && return 0.0 - i0 == length(signal) && return Float64(signal[i0]) - frac = u - i0 - return (1.0 - frac) * Float64(signal[i0]) + frac * Float64(signal[i0 + 1]) -end - -function analytic_rf_for_point_sources_3d(cfg::PAMConfig3D, sources::AbstractVector{<:EmissionSource3D}) - grid = pam_grid_3d(cfg) - ny, nz, nt = pam_Ny(cfg), pam_Nz(cfg), pam_Nt(cfg) - rf = zeros(Float32, ny, nz, nt) - for src in sources - source_signal = TranscranialFUS._source_signal(nt, cfg.dt, src) - for iy in 1:ny, iz in 1:nz - dy_src = grid.y[iy] - src.lateral_y - dz_src = grid.z[iz] - src.lateral_z - r = sqrt(src.depth^2 + dy_src^2 + dz_src^2) - for it in 1:nt - emission_t = (it - 1) * cfg.dt - r / cfg.c0 - rf[iy, iz, it] += Float32(_sample_source_signal(source_signal, emission_t, cfg.dt)) - end + recon_frequencies = if haskey(opts, "recon-frequencies-mhz") && !isempty(strip(opts["recon-frequencies-mhz"])) + parse_float_list(opts["recon-frequencies-mhz"]) .* 1e6 + else + default_recon_frequencies(sources) + end + reconstruction_mode = resolve_reconstruction_mode(opts["recon-mode"], source_model) + recon_bandwidth_hz = parse(Float64, opts["recon-bandwidth-khz"]) * 1e3 + window_config = make_window_config(opts, reconstruction_mode) + source_phase_mode = parse_source_phase_mode(opts["source-phase-mode"]) + rng_sim = Random.MersenneTwister(parse(Int, opts["random-seed"]) + 1) + source_variability = parse_source_variability(opts) + if source_model in (:squiggle, :network) + emission_meta["activity_model"] = Dict( + "activity_mode" => String(source_phase_mode), + "frequency_jitter_percent" => source_variability.frequency_jitter_fraction * 100.0, + ) end - end - return rf, grid, Dict{Symbol, Any}( - :receiver_row => receiver_row(cfg), - :receiver_cols_y => receiver_col_range_y(cfg), - :receiver_cols_z => receiver_col_range_z(cfg), - :source_indices => [source_grid_index_3d(src, cfg) for src in sources], - ) -end -function run_pam_case_3d( - c::AbstractArray{<:Real, 3}, - rho::AbstractArray{<:Real, 3}, - sources::AbstractVector{<:EmissionSource3D}, - cfg::PAMConfig3D; - frequencies::Union{Nothing, AbstractVector{<:Real}}=nothing, - bandwidth::Real=0.0, - use_gpu::Bool=false, - reconstruction_axial_step::Union{Nothing, Real}=nothing, - reconstruction_mode::Symbol=:full, - window_config::PAMWindowConfig=PAMWindowConfig(), - show_progress::Bool=false, - benchmark::Bool=false, - window_batch::Int=1, - sim_mode::Symbol=:analytic, - source_phase_mode::Symbol=:coherent, - rng::Random.AbstractRNG=Random.default_rng(), - source_variability::SourceVariabilityConfig=SourceVariabilityConfig(), -) - use_gpu || error("3D PAM reconstruction currently requires --use-gpu=true.") - recon_freqs = isnothing(frequencies) ? default_recon_frequencies(sources) : Float64.(frequencies) - phase_mode = TranscranialFUS._normalize_source_phase_mode(source_phase_mode) - recon_mode = phase_mode == :random_phase_per_window ? - :windowed : - TranscranialFUS._normalize_reconstruction_mode(reconstruction_mode) - effective_window_config = PAMWindowConfig(; - enabled=recon_mode == :windowed, - window_duration=window_config.window_duration, - hop=window_config.hop, - taper=window_config.taper, - min_energy_ratio=window_config.min_energy_ratio, - accumulation=window_config.accumulation, - ) - sim_sources = sources - n_frames = 1 - if phase_mode == :random_static_phase - sim_sources = TranscranialFUS._resample_source_phases_3d(sources, rng) - elseif phase_mode == :random_phase_per_window - sim_sources, n_frames = TranscranialFUS._expand_sources_per_window( - sources, - effective_window_config.window_duration, - effective_window_config.hop, - cfg.t_max, - rng; - variability=source_variability, - ) - elseif phase_mode == :random_phase_per_realization - error("3D PAM does not implement --source-phase-mode=random_phase_per_realization yet.") - end - rf, grid, sim_info = if sim_mode == :kwave - simulate_point_sources_3d(c, rho, sim_sources, cfg; use_gpu=use_gpu) - else - analytic_rf_for_point_sources_3d(cfg, sim_sources) - end - recon_kwargs = ( - frequencies=recon_freqs, - bandwidth=bandwidth, - reference_sound_speed=TranscranialFUS._pam_reference_sound_speed(c, cfg, sources), - axial_step=reconstruction_axial_step, - use_gpu=use_gpu, - show_progress=show_progress, - benchmark=benchmark, - window_batch=window_batch, - ) - pam_geo, _, geo_info = if recon_mode == :windowed - reconstruct_pam_windowed_3d( - rf, - c, - cfg; - recon_kwargs..., - corrected=false, - window_config=effective_window_config, - ) - else - reconstruct_pam_3d(rf, c, cfg; recon_kwargs..., corrected=false) - end - pam_hasa, _, hasa_info = if recon_mode == :windowed - reconstruct_pam_windowed_3d( - rf, + simulation_backend = parse_simulation_backend(opts["simulation-backend"]) + simulation_backend == :analytic && aberrator == :skull && + error("--simulation-backend=analytic is not compatible with --aberrator=skull; use --simulation-backend=kwave.") + results = run_pam_case_3d( c, + rho, + sources, cfg; - recon_kwargs..., - corrected=true, - window_config=effective_window_config, + frequencies=recon_frequencies, + bandwidth=recon_bandwidth_hz, + kwave_use_gpu=parse_bool(opts["kwave-use-gpu"]), + recon_use_gpu=parse_bool(opts["recon-use-gpu"]), + reconstruction_axial_step=parse(Float64, opts["recon-step-um"]) * 1e-6, + reconstruction_mode=reconstruction_mode, + window_config=window_config, + show_progress=parse_bool(opts["recon-progress"]), + benchmark=parse_bool(opts["benchmark"]), + window_batch=parse(Int, opts["window-batch"]), + simulation_backend=simulation_backend, + source_phase_mode=source_phase_mode, + rng=rng_sim, + source_variability=source_variability, ) - else - reconstruct_pam_3d(rf, c, cfg; recon_kwargs..., corrected=true) - end - - return Dict{Symbol, Any}( - :rf => Float64.(rf), - :kgrid => grid, - :simulation => sim_info, - :pam_geo => pam_geo, - :pam_hasa => pam_hasa, - :geo_info => geo_info, - :hasa_info => hasa_info, - :stats_geo => any(s -> s isa BubbleCluster3D, sources) ? Dict{Symbol,Any}() : analyse_pam_3d(pam_geo, grid, cfg, sources), - :stats_hasa => any(s -> s isa BubbleCluster3D, sources) ? Dict{Symbol,Any}() : analyse_pam_3d(pam_hasa, grid, cfg, sources), - :reconstruction_frequencies => recon_freqs, - :analysis_mode => any(s -> s isa BubbleCluster3D, sources) ? :detection : :localization, - :analysis_source_count => length(sources), - :emission_event_count => length(sim_sources), - :reconstruction_mode => recon_mode, - :source_phase_mode => phase_mode, - :n_frames => n_frames, - :window_config => TranscranialFUS._window_config_info(effective_window_config), - :use_gpu => use_gpu, - :show_progress => show_progress, - ) -end - -function json3_to_any(x) - if x isa JSON3.Object - return Dict{String, Any}(String(k) => json3_to_any(v) for (k, v) in pairs(x)) - elseif x isa JSON3.Array - return Any[json3_to_any(v) for v in x] - else - return x - end -end - -function source_model_from_meta(meta, sources) - if haskey(meta, "source_model") - model = Symbol(String(meta["source_model"])) - model == :vascular && return :squiggle - model == :squiggle3d && return :squiggle - model == :network3d && return :network - return model - end - if haskey(meta, "cluster_model") - old = Symbol(String(meta["cluster_model"])) - return old == :vascular ? :squiggle : old - end - return any(src -> src isa Union{BubbleCluster2D, GaussianPulseCluster2D}, sources) ? :squiggle : :point -end - -function centerlines_from_emission_meta(meta) - key = haskey(meta, "squiggle") ? "squiggle" : (haskey(meta, "network") ? "network" : (haskey(meta, "vascular") ? "vascular" : "")) - isempty(key) && return nothing - block = meta[key] - haskey(block, "centerlines_m") || return nothing - centerlines = Vector{Tuple{Float64, Float64}}[] - for raw_line in block["centerlines_m"] - line = Tuple{Float64, Float64}[] - for point in raw_line - push!(line, (Float64(point[1]), Float64(point[2]))) - end - length(line) >= 2 && push!(centerlines, line) - end - return isempty(centerlines) ? nothing : centerlines -end - -function detection_truth_mask_from_meta(meta, kgrid, cfg, radius::Real) - centerlines = centerlines_from_emission_meta(meta) - isnothing(centerlines) && return nothing - return pam_centerline_truth_mask(centerlines, kgrid, cfg; radius=radius) -end - -function map_db(map::AbstractMatrix{<:Real}, ref::Real) - safe_ref = max(Float64(ref), eps(Float64)) - return 10 .* log10.(max.(Float64.(map), eps(Float64)) ./ safe_ref) -end - -function map_norm(map::AbstractMatrix{<:Real}, ref::Real) - safe_ref = max(Float64(ref), eps(Float64)) - return Float64.(map) ./ safe_ref -end - -function source_pairs_mm(sources) - return [(src.depth * 1e3, src.lateral * 1e3) for src in sources] -end - -function source_triples_mm(sources::AbstractVector{<:EmissionSource3D}) - return [(src.depth * 1e3, src.lateral_y * 1e3, src.lateral_z * 1e3) for src in sources] -end - -function scatter_sources!(ax, sources; color=:red, marker=nothing, markersize=nothing, strokewidth=2) - truth = source_pairs_mm(sources) - marker = isnothing(marker) ? (length(sources) > 20 ? :circle : :x) : marker - markersize = isnothing(markersize) ? (length(sources) > 20 ? 4 : 14) : markersize - scatter!( - ax, - last.(truth), - first.(truth); - color=color, - marker=marker, - markersize=markersize, - strokewidth=strokewidth, - ) -end - -function save_overview(path, c, rf, pam_geo, pam_hasa, kgrid, cfg, sources, stats_geo, stats_hasa) - depth_mm = depth_coordinates(kgrid, cfg) .* 1e3 - lateral_mm = kgrid.y_vec .* 1e3 - time_us = collect(0:(size(rf, 2) - 1)) .* cfg.dt .* 1e6 - map_ref = max(maximum(Float64.(pam_geo)), maximum(Float64.(pam_hasa)), eps(Float64)) - - fig = Figure(size=(1500, 1000)) - ax_medium = Axis(fig[1, 1]; title="Simulation Medium", xlabel="Lateral position [mm]", ylabel="Depth below receiver [mm]", aspect=DataAspect()) - hm_medium = heatmap!(ax_medium, lateral_mm, depth_mm, Float64.(c)'; colormap=:thermal) - hlines!(ax_medium, [0.0]; color=:white, linestyle=:dash) - scatter_sources!(ax_medium, sources) - Colorbar(fig[1, 2], hm_medium; label="Sound speed [m/s]") - - ax_rf = Axis(fig[1, 3]; title="Recorded RF Data", xlabel="Time [us]", ylabel="Lateral position [mm]") - rf_ref = max(maximum(abs.(rf)), eps(Float64)) - hm_rf = heatmap!(ax_rf, time_us, lateral_mm, Float64.(rf ./ rf_ref)'; colormap=:balance, colorrange=(-1, 1)) - Colorbar(fig[1, 4], hm_rf; label="Norm. pressure") - - ax_geo = Axis(fig[2, 1]; title="Geometric ASA PAM", xlabel="Lateral position [mm]", ylabel="Depth below receiver [mm]", aspect=DataAspect()) - hm_geo = heatmap!(ax_geo, lateral_mm, depth_mm, map_db(pam_geo, map_ref)'; colormap=:viridis, colorrange=(-30, 0)) - overlay_skull_2d!(ax_geo, c, lateral_mm, depth_mm) - scatter_sources!(ax_geo, sources) - Colorbar(fig[2, 2], hm_geo; label="dB") - - ax_hasa = Axis(fig[2, 3]; title="Corrected HASA PAM", xlabel="Lateral position [mm]", ylabel="Depth below receiver [mm]", aspect=DataAspect()) - hm_hasa = heatmap!(ax_hasa, lateral_mm, depth_mm, map_db(pam_hasa, map_ref)'; colormap=:viridis, colorrange=(-30, 0)) - overlay_skull_2d!(ax_hasa, c, lateral_mm, depth_mm) - scatter_sources!(ax_hasa, sources) - Colorbar(fig[2, 4], hm_hasa; label="dB") - metrics_text = join([ - "Geometric: $(summary_line(stats_geo))", - "Corrected: $(summary_line(stats_hasa))", - ], "\n") - Label(fig[3, 1:4], metrics_text; tellwidth=false, halign=:left) - save(path, fig) -end - -function summary_line(stats) - if haskey(stats, :mean_radial_error_mm) - return "mean err=$(round(stats[:mean_radial_error_mm]; digits=2)) mm, success=$(get(stats, :num_success, 0))/$(get(stats, :num_truth_sources, 0))" - elseif haskey(stats, :f1) - return "F1=$(round(stats[:f1]; digits=3)), precision=$(round(stats[:precision]; digits=3)), recall=$(round(stats[:recall]; digits=3))" - end - return string(stats) -end - -string_key_dict(d::AbstractDict) = Dict(String(k) => v for (k, v) in d) + medium_summary = TranscranialFUS.run_pam_medium_summary(medium_info) + + activity_boundary_path = joinpath(out_dir, "activity_boundaries.png") + activity_boundary_metrics = save_threshold_boundary_detection_3d( + activity_boundary_path, + results[:pam_geo], + results[:pam_hasa], + results[:kgrid], + cfg, + sources; + threshold_ratios=threshold_score_ratios, + truth_radius=detection_truth_radius_m, + c=c, + ) + activity_boundary_metrics["auto_threshold_search"] = auto_threshold_search + activity_boundary_metrics["display_threshold_mode"] = "selected_best_recall_precision" + best_volume_path = joinpath(out_dir, "best_threshold_3d.png") + best_volume_metrics = save_best_threshold_volume_3d( + best_volume_path, + results[:pam_hasa], + results[:kgrid], + cfg, + sources; + threshold=activity_boundary_metrics["best_hasa_threshold"], + truth_radius=detection_truth_radius_m, + ) -function threshold_detection_stats(intensity, kgrid, cfg, sources; threshold_ratios, truth_radius, truth_mask, frequencies) - return [ - merge( - Dict(:threshold_ratio => ratio), - analyse_pam_detection_2d( - intensity, - kgrid, - cfg, - sources; - truth_radius=truth_radius, - threshold_ratio=ratio, - truth_mask=truth_mask, - frequencies=frequencies, + summary = Dict( + "out_dir" => out_dir, + "dimension" => 3, + "reconstruction_source" => Dict("mode" => String(simulation_backend)), + "simulation_backend" => String(simulation_backend), + "activity_boundary_figure" => activity_boundary_path, + "activity_boundary_metrics" => activity_boundary_metrics, + "best_threshold_3d_figure" => best_volume_path, + "best_threshold_3d_metrics" => best_volume_metrics, + "sources" => [source_summary(src) for src in sources], + "emission_meta" => emission_meta, + "config" => Dict( + "dx" => cfg.dx, + "dy" => cfg.dy, + "dz" => cfg.dz, + "axial_dim" => cfg.axial_dim, + "transverse_dim_y" => cfg.transverse_dim_y, + "transverse_dim_z" => cfg.transverse_dim_z, + "receiver_aperture_y" => cfg.receiver_aperture_y, + "receiver_aperture_z" => cfg.receiver_aperture_z, + "t_max" => cfg.t_max, + "dt" => cfg.dt, + "c0" => cfg.c0, + "rho0" => cfg.rho0, + "zero_pad_factor" => cfg.zero_pad_factor, + "peak_suppression_radius" => cfg.peak_suppression_radius, + "success_tolerance" => cfg.success_tolerance, + "axial_gain_power" => cfg.axial_gain_power, + "bottom_margin" => bottom_margin_m, + ), + "medium" => medium_summary, + "reconstruction_frequencies_hz" => recon_frequencies, + "reconstruction_bandwidth_hz" => recon_bandwidth_hz, + "reconstruction_mode" => String(results[:reconstruction_mode]), + "reconstruction_progress" => parse_bool(opts["recon-progress"]), + "source_phase_mode" => String(results[:source_phase_mode]), + "n_frames" => Int(get(results, :n_frames, 1)), + "source_variability" => Dict( + "frequency_jitter_percent" => source_variability.frequency_jitter_fraction * 100.0, + ), + "threshold_search" => Dict( + "auto" => auto_threshold_search, + "min_ratio" => minimum(threshold_score_ratios), + "max_ratio" => maximum(threshold_score_ratios), + "step" => auto_threshold_search ? parse(Float64, opts["auto-threshold-step"]) : nothing, + "count" => length(threshold_score_ratios), + "selection_metric" => "source_f1", + "display_threshold_mode" => "selected_best_recall_precision", + ), + "physical_source_count" => length(sources), + "emission_event_count" => Int(get(results, :emission_event_count, length(sources))), + "window_config" => string_key_dict(results[:window_config]), + "window_info" => Dict( + "geometric" => compact_window_info(results[:geo_info]), + "hasa" => compact_window_info(results[:hasa_info]), ), + "benchmark" => parse_bool(opts["benchmark"]), + "gpu_timing" => Dict( + "geometric" => get(results[:geo_info], :gpu_timing, nothing), + "hasa" => get(results[:hasa_info], :gpu_timing, nothing), + ), + "reconstruction_axial_step_m" => results[:geo_info][:axial_step], + "reference_sound_speed_m_per_s" => results[:geo_info][:reference_sound_speed], + "analysis_mode" => String(results[:analysis_mode]), + "simulation" => Dict( + "receiver_row" => results[:simulation][:receiver_row], + "receiver_cols_y" => [first(results[:simulation][:receiver_cols_y]), last(results[:simulation][:receiver_cols_y])], + "receiver_cols_z" => [first(results[:simulation][:receiver_cols_z]), last(results[:simulation][:receiver_cols_z])], + "source_indices" => [[row, col_y, col_z] for (row, col_y, col_z) in get(results[:simulation], :source_indices, NTuple{3, Int}[])], + ), + "geometric" => results[:stats_geo], + "hasa" => results[:stats_hasa], ) - for ratio in threshold_ratios - ] -end -function source_detection_stats_3d(pred, grid, cfg::PAMConfig3D, sources; radius::Real) - radius_m = Float64(radius) - radius_m >= 0 || error("source detection radius must be non-negative.") - isempty(sources) && return Dict{Symbol, Any}( - :source_recall => 0.0, - :detected_source_count => 0, - :num_truth_sources => 0, - :mean_detected_source_distance_mm => nothing, - :max_detected_source_distance_mm => nothing, - ) - x = collect(grid.x) - y = collect(grid.y) - z = collect(grid.z) - r0 = receiver_row(cfg) - row_r = ceil(Int, radius_m / cfg.dx) - col_r_y = ceil(Int, radius_m / cfg.dy) - col_r_z = ceil(Int, radius_m / cfg.dz) - radius2 = radius_m^2 - - detected = 0 - distances_mm = Float64[] - for src in sources - src_x = x[r0] + src.depth - row0 = r0 + round(Int, src.depth / cfg.dx) - col0_y = argmin(abs.(y .- src.lateral_y)) - col0_z = argmin(abs.(z .- src.lateral_z)) - best_d2 = Inf - for row in max(r0 + 1, row0 - row_r):min(size(pred, 1), row0 + row_r) - dx2 = (x[row] - src_x)^2 - for iy in max(1, col0_y - col_r_y):min(size(pred, 2), col0_y + col_r_y) - dy2 = (y[iy] - src.lateral_y)^2 - for iz in max(1, col0_z - col_r_z):min(size(pred, 3), col0_z + col_r_z) - pred[row, iy, iz] || continue - d2 = dx2 + dy2 + (z[iz] - src.lateral_z)^2 - if d2 <= radius2 && d2 < best_d2 - best_d2 = d2 - end - end - end + open(joinpath(out_dir, "summary.json"), "w") do io + JSON3.pretty(io, summary) end - if isfinite(best_d2) - detected += 1 - push!(distances_mm, sqrt(best_d2) * 1e3) - end - end - - return Dict{Symbol, Any}( - :source_recall => detected / length(sources), - :detected_source_count => detected, - :num_truth_sources => length(sources), - :mean_detected_source_distance_mm => isempty(distances_mm) ? nothing : mean(distances_mm), - :max_detected_source_distance_mm => isempty(distances_mm) ? nothing : maximum(distances_mm), - ) -end -function threshold_detection_stats_3d(intensity, grid, cfg, sources; threshold_ratios, truth_radius, truth_mask) - truth = isnothing(truth_mask) ? pam_truth_mask_3d(sources, grid, cfg; radius=truth_radius) : truth_mask - local_ref = max(maximum(Float64.(intensity)), eps(Float64)) - return [ - begin - pred = intensity .>= ratio * local_ref - tp = count(pred .& truth) - fp = count(pred .& .!truth) - fn = count(.!pred .& truth) - precision = tp + fp == 0 ? 0.0 : tp / (tp + fp) - recall = tp + fn == 0 ? 0.0 : tp / (tp + fn) - f1 = precision + recall == 0 ? 0.0 : 2 * precision * recall / (precision + recall) - source_stats = source_detection_stats_3d(pred, grid, cfg, sources; radius=truth_radius) - source_recall = Float64(source_stats[:source_recall]) - source_f1 = precision + source_recall == 0 ? 0.0 : 2 * precision * source_recall / (precision + source_recall) - merge(Dict( - :threshold_ratio => ratio, - :f1 => source_f1, - :source_f1 => source_f1, - :voxel_f1 => f1, - :precision => precision, - :recall => source_recall, - :voxel_recall => recall, - :true_positive_voxels => tp, - :false_positive_voxels => fp, - :false_negative_voxels => fn, - :predicted_voxels => count(pred), - :truth_voxels => count(truth), - ), source_stats) - end - for ratio in threshold_ratios - ] -end + @save joinpath(out_dir, "result.jld2") c rho cfg sources results medium_info + + save_napari_npz_3d( + out_dir, + results[:pam_geo], + results[:pam_hasa], + c, rho, + results[:kgrid], + cfg, + sources; + truth_radius=detection_truth_radius_m, + ) -function best_threshold_entry_3d(stats) - isempty(stats) && error("No 3D threshold stats available.") - best = first(stats) - for entry in stats[2:end] - score_metric = haskey(entry, :source_f1) ? :source_f1 : :f1 - best_metric = haskey(best, :source_f1) ? :source_f1 : :f1 - score = (Float64(entry[score_metric]), Float64(entry[:precision]), Float64(entry[:threshold_ratio])) - best_score = (Float64(best[best_metric]), Float64(best[:precision]), Float64(best[:threshold_ratio])) - if score > best_score - best = entry - end - end - return best -end + println("Saved 3D PAM outputs to $out_dir") + return 0 + end + + if isempty(from_run_dir) + cfg_base = PAMConfig( + dx=parse(Float64, opts["dx-mm"]) * 1e-3, + dz=parse(Float64, opts["dz-mm"]) * 1e-3, + axial_dim=parse(Float64, opts["axial-mm"]) * 1e-3, + transverse_dim=parse(Float64, opts["transverse-mm"]) * 1e-3, + receiver_aperture=parse_receiver_aperture_mm(opts["receiver-aperture-mm"]), + t_max=parse(Float64, opts["t-max-us"]) * 1e-6, + dt=parse(Float64, opts["dt-ns"]) * 1e-9, + zero_pad_factor=parse(Int, opts["zero-pad-factor"]), + peak_suppression_radius=parse(Float64, opts["peak-suppression-radius-mm"]) * 1e-3, + success_tolerance=parse(Float64, opts["success-tolerance-mm"]) * 1e-3, + ) -_metric_value(entry, key::Symbol, fallback::Symbol=key) = Float64(get(entry, key, entry[fallback])) + sources, emission_meta = parse_sources(opts, cfg_base) + source_model = source_model_from_meta(emission_meta, sources) -function _argmax_by(entries, scorefn) - best = first(entries) - best_score = scorefn(best) - for entry in entries[2:end] - score = scorefn(entry) - if score > best_score - best = entry - best_score = score - end - end - return best -end + aberrator = parse_aberrator(opts["aberrator"]) + bottom_margin_m = parse(Float64, opts["bottom-margin-mm"]) * 1e-3 + cfg = fit_pam_config( + cfg_base, + sources; + min_bottom_margin=bottom_margin_m, + reference_depth=aberrator == :skull ? parse(Float64, opts["skull-transducer-distance-mm"]) * 1e-3 : nothing, + ) -function _threshold_tradeoff_entry_3d(stats, best, target::Symbol) - best_f1 = _metric_value(best, :source_f1, :f1) - best_value = _metric_value(best, target, target == :source_recall ? :recall : target) - metric_key = target == :source_recall ? :source_recall : :precision - fallback_key = target == :source_recall ? :recall : :precision - candidates = [entry for entry in stats if _metric_value(entry, metric_key, fallback_key) > best_value + 1e-9] - for floor_fraction in (0.95, 0.90, 0.0) - viable = [entry for entry in candidates if _metric_value(entry, :source_f1, :f1) >= floor_fraction * best_f1] - isempty(viable) && continue - if target == :source_recall - return _argmax_by(viable, entry -> ( - _metric_value(entry, :source_recall, :recall), - _metric_value(entry, :source_f1, :f1), - _metric_value(entry, :precision), - )) + out_dir = if haskey(opts, "out-dir") && !isempty(strip(opts["out-dir"])) + opts["out-dir"] else - return _argmax_by(viable, entry -> ( - _metric_value(entry, :precision), - _metric_value(entry, :source_f1, :f1), - _metric_value(entry, :source_recall, :recall), - )) + default_output_dir(opts, sources, cfg, emission_meta) end - end - return best -end - -function threshold_outline_entries_3d(stats) - best = best_threshold_entry_3d(stats) - recall = _threshold_tradeoff_entry_3d(stats, best, :source_recall) - precision = _threshold_tradeoff_entry_3d(stats, best, :precision) - return [ - (kind=:best_f1, label="best F1", color=:cyan, entry=best), - (kind=:more_recall, label="more recall", color=:lime, entry=recall), - (kind=:more_precision, label="more precision", color=:magenta, entry=precision), - ] -end - -function overlay_skull_2d!(ax, c, xvals, yvals; transpose_matrix=true) - skull_mask = skull_mask_from_c_columnwise(c; mask_outside=false) - any(skull_mask) || return nothing - c_max = Float64(maximum(c[skull_mask])) - overlay = fill(NaN, size(c)) - overlay[skull_mask] .= Float64.(c[skull_mask]) ./ c_max - mat = transpose_matrix ? overlay' : overlay - heatmap!(ax, xvals, yvals, mat; colormap=:grays, alpha=0.35, colorrange=(0, 1), nan_color=CairoMakie.RGBAf(0, 0, 0, 0)) - return nothing -end - -function _c_slice_for_projection(c::AbstractArray{<:Real, 3}, projection::Symbol) - if projection == :depth_y - return dropdims(maximum(c; dims=3), dims=3) - elseif projection == :depth_z - return dropdims(maximum(c; dims=2), dims=2) - else # :y_z - return dropdims(maximum(c; dims=1), dims=1) - end -end + mkpath(out_dir) -function overlay_skull_3d_projection!(ax, c::AbstractArray{<:Real, 3}, xvals, yvals, projection::Symbol) - c2d = _c_slice_for_projection(c, projection) - # :y_z projection is not transposed (matches _projection_heatmap_matrix_3d convention) - overlay_skull_2d!(ax, c2d, xvals, yvals; transpose_matrix=(projection != :y_z)) - return nothing -end - -function lines_centerlines!(ax, centerlines; color=(:black, 0.45), linewidth=2) - isnothing(centerlines) && return nothing - for line in centerlines - length(line) >= 2 || continue - lines!(ax, [point[2] * 1e3 for point in line], [point[1] * 1e3 for point in line]; color=color, linewidth=linewidth) - end - return nothing -end - -function add_threshold_panel!( - fig, - row, - title, - intensity, - kgrid, - cfg, - sources; - threshold_ratios, - colors, - global_ref, - truth_mask, - truth_centerlines, - c=nothing, -) - depth_mm = depth_coordinates(kgrid, cfg) .* 1e3 - lateral_mm = kgrid.y_vec .* 1e3 - ax = Axis(fig[row, 1]; title=title, xlabel="Lateral [mm]", ylabel="Depth [mm]", aspect=DataAspect()) - hm = heatmap!(ax, lateral_mm, depth_mm, Float64.(intensity ./ global_ref)'; colormap=:viridis, colorrange=(0, 1)) - !isnothing(c) && overlay_skull_2d!(ax, c, lateral_mm, depth_mm) - if !isnothing(truth_mask) && any(truth_mask) && any(.!truth_mask) - contour!(ax, lateral_mm, depth_mm, Float64.(truth_mask)'; levels=[0.5], color=(:white, 0.85), linewidth=2.3, linestyle=:dash) - end - lines_centerlines!(ax, truth_centerlines; color=(:white, 0.7), linewidth=1.3) - local_ref = max(maximum(Float64.(intensity)), eps(Float64)) - for (idx, ratio) in pairs(threshold_ratios) - contour!(ax, lateral_mm, depth_mm, Float64.(intensity .>= ratio * local_ref)'; levels=[0.5], color=colors[idx], linewidth=2) - end - scatter_sources!(ax, sources; color=(:white, 0.55), marker=:circle, markersize=2.5, strokewidth=0) - return hm -end + c, rho, medium_info = make_pam_medium( + cfg; + aberrator=aberrator, + ct_path=opts["ct-path"], + slice_index=parse(Int, opts["slice-index"]), + skull_to_transducer=parse(Float64, opts["skull-transducer-distance-mm"]) * 1e-3, + hu_bone_thr=parse(Int, opts["hu-bone-thr"]), + ) -function add_threshold_table!(fig, row, col, title, stats) - gl = GridLayout(fig[row, col]; tellwidth=false, tellheight=true) - Label(gl[1, 1:4], title; font="DejaVu Sans Mono", fontsize=13, halign=:left, tellwidth=false) - headers = ["thr", "F1", "Prec", "Recall"] - for (c, h) in enumerate(headers) - Label(gl[2, c], h; font="DejaVu Sans Mono", fontsize=11, halign=:center) - end - for (r, entry) in enumerate(stats) - vals = [ - @sprintf("%.2f", Float64(entry[:threshold_ratio])), - @sprintf("%.3f", Float64(entry[:f1])), - @sprintf("%.3f", Float64(entry[:precision])), - @sprintf("%.3f", Float64(entry[:recall])), - ] - for (c, v) in enumerate(vals) - Label(gl[2 + r, c], v; font="DejaVu Sans Mono", fontsize=11, halign=:center) + recon_frequencies = if haskey(opts, "recon-frequencies-mhz") && !isempty(strip(opts["recon-frequencies-mhz"])) + parse_float_list(opts["recon-frequencies-mhz"]) .* 1e6 + else + default_recon_frequencies(sources) end - end - colgap!(gl, 10) - rowgap!(gl, 2) -end - -function _project3d_values(intensity::AbstractArray{<:Real, 3}, projection::Symbol) - values = Float64.(intensity) - if projection == :depth_y - return dropdims(maximum(values; dims=3), dims=3) - elseif projection == :depth_z - return dropdims(maximum(values; dims=2), dims=2) - elseif projection == :y_z - return dropdims(maximum(values; dims=1), dims=1) - end - error("Unknown 3D projection: $projection") -end - -function _project3d_mask(mask::AbstractArray{Bool, 3}, projection::Symbol) - if projection == :depth_y - return dropdims(any(mask; dims=3), dims=3) - elseif projection == :depth_z - return dropdims(any(mask; dims=2), dims=2) - elseif projection == :y_z - return dropdims(any(mask; dims=1), dims=1) - end - error("Unknown 3D projection: $projection") -end - -function _projection_axes_3d(grid, cfg::PAMConfig3D, projection::Symbol) - depth_mm = depth_coordinates_3d(cfg) .* 1e3 - y_mm = collect(grid.y) .* 1e3 - z_mm = collect(grid.z) .* 1e3 - if projection == :depth_y - return y_mm, depth_mm, "Y [mm]", "Depth [mm]" - elseif projection == :depth_z - return z_mm, depth_mm, "Z [mm]", "Depth [mm]" - elseif projection == :y_z - return y_mm, z_mm, "Y [mm]", "Z [mm]" - end - error("Unknown 3D projection: $projection") -end - -function _projection_heatmap_matrix_3d(values::AbstractMatrix, projection::Symbol) - projection == :y_z && return values - return values' -end - -function scatter_sources_3d_projection!(ax, sources, projection::Symbol; color=(:white, 0.75)) - truth = source_triples_mm(sources) - if projection == :depth_y - scatter!(ax, [t[2] for t in truth], [t[1] for t in truth]; color=color, marker=:x, markersize=13, strokewidth=2) - elseif projection == :depth_z - scatter!(ax, [t[3] for t in truth], [t[1] for t in truth]; color=color, marker=:x, markersize=13, strokewidth=2) - elseif projection == :y_z - scatter!(ax, [t[2] for t in truth], [t[3] for t in truth]; color=color, marker=:x, markersize=13, strokewidth=2) - end - return nothing -end - -function add_projection_panel_3d!( - fig, - row, - col, - title, - intensity, - truth_mask, - grid, - cfg, - sources; - projection::Symbol, - outline_entries, - global_ref, - c=nothing, -) - xvals, yvals, xlabel, ylabel = _projection_axes_3d(grid, cfg, projection) - proj = _project3d_values(intensity, projection) - truth_proj = _project3d_mask(truth_mask, projection) - ax = Axis(fig[row, col]; title=title, xlabel=xlabel, ylabel=ylabel, aspect=DataAspect()) - hm = heatmap!( - ax, - xvals, - yvals, - _projection_heatmap_matrix_3d(map_norm(proj, global_ref), projection); - colormap=:viridis, - colorrange=(0, 1), - ) - !isnothing(c) && overlay_skull_3d_projection!(ax, c, xvals, yvals, projection) - if any(truth_proj) && any(.!truth_proj) - contour!( - ax, - xvals, - yvals, - _projection_heatmap_matrix_3d(Float64.(truth_proj), projection); - levels=[0.5], - color=(:white, 0.85), - linewidth=2.4, - linestyle=:dash, - ) - end - local_ref = max(maximum(Float64.(intensity)), eps(Float64)) - for outline in outline_entries - ratio = Float64(outline.entry[:threshold_ratio]) - pred_proj = _project3d_mask(intensity .>= ratio * local_ref, projection) - if any(pred_proj) && any(.!pred_proj) - contour!( - ax, - xvals, - yvals, - _projection_heatmap_matrix_3d(Float64.(pred_proj), projection); - levels=[0.5], - color=outline.color, - linewidth=2, + reconstruction_mode = resolve_reconstruction_mode(opts["recon-mode"], source_model) + recon_bandwidth_hz = parse(Float64, opts["recon-bandwidth-khz"]) * 1e3 + window_config = make_window_config(opts, reconstruction_mode) + analysis_mode = parse_analysis_mode(opts["analysis-mode"], source_model) + truth_centerlines = centerlines_from_emission_meta(emission_meta) + detection_truth_mask = detection_truth_mask_from_meta(emission_meta, pam_grid(cfg), cfg, detection_truth_radius_m) + + source_phase_mode = parse_source_phase_mode(opts["source-phase-mode"]) + rng_sim = Random.MersenneTwister(parse(Int, opts["random-seed"]) + 1) + source_variability = parse_source_variability(opts) + if source_model == :squiggle + emission_meta["activity_model"] = Dict( + "activity_mode" => String(source_phase_mode), + "frequency_jitter_percent" => source_variability.frequency_jitter_fraction * 100.0, ) end - end - scatter_sources_3d_projection!(ax, sources, projection) - return hm -end -function add_threshold_table_3d!(fig, row, col, title, stats; outline_entries=nothing) - rows_data = isnothing(outline_entries) ? [(label="", entry=entry) for entry in stats] : - [(label=outline.label, entry=outline.entry) for outline in outline_entries] - gl = GridLayout(fig[row, col]; tellwidth=false, tellheight=true) - Label(gl[1, 1:8], title; font="DejaVu Sans Mono", fontsize=13, halign=:left, tellwidth=false) - headers = ["", "thr", "SrcF1", "Prec", "SrcRc", "VoxF1", "Vox"] - for (c, h) in enumerate(headers) - Label(gl[2, c], h; font="DejaVu Sans Mono", fontsize=11, halign=c == 1 ? :right : :center) - end - for (r, row_entry) in enumerate(rows_data) - entry = row_entry.entry - vals = [ - row_entry.label, - @sprintf("%.2f", Float64(entry[:threshold_ratio])), - @sprintf("%.3f", Float64(get(entry, :source_f1, entry[:f1]))), - @sprintf("%.3f", Float64(entry[:precision])), - @sprintf("%.3f", Float64(get(entry, :source_recall, entry[:recall]))), - @sprintf("%.3f", Float64(get(entry, :voxel_f1, entry[:f1]))), - @sprintf("%d", Int(entry[:predicted_voxels])), - ] - for (c, v) in enumerate(vals) - Label(gl[2 + r, c], v; font="DejaVu Sans Mono", fontsize=11, halign=c == 1 ? :right : :center) - end - end - colgap!(gl, 10) - rowgap!(gl, 2) -end - -function add_threshold_curve_panel_3d!(fig, row, col, title, stats; outline_entries) - thresholds = [Float64(entry[:threshold_ratio]) for entry in stats] - f1 = [Float64(get(entry, :source_f1, entry[:f1])) for entry in stats] - precision = [Float64(entry[:precision]) for entry in stats] - recall = [Float64(get(entry, :source_recall, entry[:recall])) for entry in stats] - ax = Axis(fig[row, col]; title=title, xlabel="Threshold / max intensity", ylabel="Score") - lines!(ax, thresholds, f1; color=:cyan, linewidth=2.5, label="source F1") - lines!(ax, thresholds, precision; color=:magenta, linewidth=2.0, label="precision") - lines!(ax, thresholds, recall; color=:lime, linewidth=2.0, label="source recall") - for outline in outline_entries - threshold = Float64(outline.entry[:threshold_ratio]) - lines!(ax, [threshold, threshold], [0.0, 1.0]; color=(outline.color, 0.45), linewidth=1.5, linestyle=:dash) - end - ylims!(ax, 0, 1) - axislegend(ax; position=:rb, framevisible=false) - return nothing -end - -function save_threshold_boundary_detection(path, pam_geo, pam_hasa, kgrid, cfg, sources; threshold_ratios, truth_radius, truth_mask, truth_centerlines, frequencies, c=nothing) - global_ref = max(maximum(Float64.(pam_geo)), maximum(Float64.(pam_hasa)), eps(Float64)) - colors = [:red, :orange, :cyan, :magenta, :lime] - while length(colors) < length(threshold_ratios) - append!(colors, colors) - end - geo_stats = threshold_detection_stats(pam_geo, kgrid, cfg, sources; threshold_ratios=threshold_ratios, truth_radius=truth_radius, truth_mask=truth_mask, frequencies=frequencies) - hasa_stats = threshold_detection_stats(pam_hasa, kgrid, cfg, sources; threshold_ratios=threshold_ratios, truth_radius=truth_radius, truth_mask=truth_mask, frequencies=frequencies) - - fig = Figure(size=(1000, 1300)) - hm = add_threshold_panel!( - fig, - 1, - "Uncorrected activity regions", - pam_geo, - kgrid, - cfg, - sources; - threshold_ratios=threshold_ratios, - colors=colors, - global_ref=global_ref, - truth_mask=truth_mask, - truth_centerlines=truth_centerlines, - c=c, - ) - add_threshold_panel!( - fig, - 2, - "Corrected activity regions", - pam_hasa, - kgrid, - cfg, - sources; - threshold_ratios=threshold_ratios, - colors=colors, - global_ref=global_ref, - truth_mask=truth_mask, - truth_centerlines=truth_centerlines, - c=c, - ) - Colorbar(fig[1:2, 2], hm; label="Norm. PAM intensity") - legend_elements = [LineElement(color=colors[i], linewidth=3) for i in eachindex(threshold_ratios)] - legend_labels = ["thr=$(round(r; digits=2))" for r in threshold_ratios] - Legend(fig[3, 1], legend_elements, legend_labels; orientation=:horizontal, tellheight=true, framevisible=false) - add_threshold_table!(fig, 4, 1, "Uncorrected quantitative region metrics", geo_stats) - add_threshold_table!(fig, 5, 1, "Corrected quantitative region metrics", hasa_stats) - save(path, fig) - return Dict( - "threshold_ratios" => threshold_ratios, - "geometric" => [string_key_dict(stats) for stats in geo_stats], - "hasa" => [string_key_dict(stats) for stats in hasa_stats], - ) -end - -function save_threshold_boundary_detection_3d(path, pam_geo, pam_hasa, grid, cfg, sources; threshold_ratios, truth_radius, c=nothing) - truth_mask = pam_truth_mask_3d(sources, grid, cfg; radius=truth_radius) - geo_stats = threshold_detection_stats_3d(pam_geo, grid, cfg, sources; threshold_ratios=threshold_ratios, truth_radius=truth_radius, truth_mask=truth_mask) - hasa_stats = threshold_detection_stats_3d(pam_hasa, grid, cfg, sources; threshold_ratios=threshold_ratios, truth_radius=truth_radius, truth_mask=truth_mask) - global_ref = max(maximum(Float64.(pam_geo)), maximum(Float64.(pam_hasa)), eps(Float64)) - best_geo = best_threshold_entry_3d(geo_stats) - best_hasa = best_threshold_entry_3d(hasa_stats) - geo_outlines = threshold_outline_entries_3d(geo_stats) - hasa_outlines = threshold_outline_entries_3d(hasa_stats) - - fig = Figure(size=(1550, 1450)) - projections = (:depth_y, :depth_z, :y_z) - titles = Dict( - :depth_y => "Depth-Y max projection", - :depth_z => "Depth-Z max projection", - :y_z => "Y-Z max projection", - ) - hm = nothing - for (col, projection) in pairs(projections) - hm = add_projection_panel_3d!( - fig, 1, col, "Geometric: $(titles[projection])", - pam_geo, truth_mask, grid, cfg, sources; - projection=projection, - outline_entries=geo_outlines, - global_ref=global_ref, - c=c, + results = run_pam_case( + c, + rho, + sources, + cfg; + frequencies=recon_frequencies, + bandwidth=recon_bandwidth_hz, + use_gpu=parse_bool(opts["recon-use-gpu"]), + kwave_use_gpu=parse_bool(opts["kwave-use-gpu"]), + reconstruction_axial_step=parse(Float64, opts["recon-step-um"]) * 1e-6, + analysis_mode=analysis_mode, + detection_truth_radius=detection_truth_radius_m, + detection_threshold_ratio=detection_threshold_ratio, + detection_truth_mask=detection_truth_mask, + reconstruction_mode=reconstruction_mode, + window_config=window_config, + source_phase_mode=source_phase_mode, + rng=rng_sim, + source_variability=source_variability, + show_progress=parse_bool(opts["recon-progress"]), + benchmark=parse_bool(opts["benchmark"]), + window_batch=parse(Int, opts["window-batch"]), ) - add_projection_panel_3d!( - fig, 2, col, "HASA: $(titles[projection])", - pam_hasa, truth_mask, grid, cfg, sources; - projection=projection, - outline_entries=hasa_outlines, - global_ref=global_ref, - c=c, + reconstruction_source = Dict("mode" => "simulation") + else + reject_cached_simulation_options!( + provided_keys, + ( + "source-model", "sources-mm", "anchors-mm", "frequency-mhz", "fundamental-mhz", + "amplitude-pa", "source-amplitudes-pa", "source-frequencies-mhz", "phases-deg", + "num-cycles", "harmonics", "harmonic-amplitudes", + "gate-us", "taper-ratio", "axial-mm", "transverse-mm", "dx-mm", "dz-mm", + "receiver-aperture-mm", "t-max-us", "dt-ns", "zero-pad-factor", + "peak-suppression-radius-mm", "success-tolerance-mm", "aberrator", "ct-path", + "slice-index", "skull-transducer-distance-mm", "bottom-margin-mm", "hu-bone-thr", + "simulation-backend", "phase-mode", "phase-jitter-rad", "random-seed", + "transducer-mm", "delays-us", "vascular-length-mm", "vascular-squiggle-amplitude-mm", + "vascular-squiggle-amplitude-x-mm", "vascular-squiggle-wavelength-mm", + "vascular-squiggle-slope", "squiggle-phase-x-deg", + "vascular-source-spacing-mm", "vascular-position-jitter-mm", + "vascular-min-separation-mm", "vascular-max-sources-per-anchor", + "network-axial-radius-mm", "network-lateral-y-radius-mm", + "network-lateral-z-radius-mm", "network-root-count", "network-generations", + "network-branch-length-mm", "network-branch-step-mm", "network-branch-angle-deg", + "network-tortuosity", "network-orientation", "network-density-sigma-mm", "network-density-axial-sigma-mm", + "network-density-lateral-y-sigma-mm", "network-density-lateral-z-sigma-mm", + "network-max-sources-per-center", + "source-phase-mode", "frequency-jitter-percent", + ), ) - end - Colorbar(fig[1:2, 4], hm; label="Norm. PAM intensity") - legend_specs = [ - (label="best F1", color=:cyan), - (label="more recall", color=:lime), - (label="more precision", color=:magenta), - ] - legend_elements = [LineElement(color=spec.color, linewidth=3) for spec in legend_specs] - legend_labels = [spec.label for spec in legend_specs] - Legend(fig[3, 1:2], legend_elements, legend_labels; orientation=:horizontal, tellheight=true, framevisible=false) - Label(fig[3, 3], "Truth mask shown as dashed white contours; sources are x markers. Curves use the dense threshold search grid."; tellwidth=false, halign=:left) - add_threshold_curve_panel_3d!(fig, 4, 1:2, "Geometric threshold response", geo_stats; outline_entries=geo_outlines) - add_threshold_curve_panel_3d!(fig, 4, 3:4, "HASA threshold response", hasa_stats; outline_entries=hasa_outlines) - add_threshold_table_3d!(fig, 5, 1:2, "Geometric selected thresholds", geo_stats; outline_entries=geo_outlines) - add_threshold_table_3d!(fig, 5, 3:4, "HASA selected thresholds", hasa_stats; outline_entries=hasa_outlines) - save(path, fig) - return Dict( - "threshold_ratios" => threshold_ratios, - "selection_metric" => "source_f1", - "source_detection_radius_m" => Float64(truth_radius), - "best_geometric_threshold" => best_geo[:threshold_ratio], - "best_geometric_metric" => string_key_dict(best_geo), - "best_hasa_threshold" => best_hasa[:threshold_ratio], - "best_hasa_metric" => string_key_dict(best_hasa), - "geometric_selected_outlines" => [ - Dict("kind" => String(outline.kind), "label" => outline.label, "metric" => string_key_dict(outline.entry)) - for outline in geo_outlines - ], - "hasa_selected_outlines" => [ - Dict("kind" => String(outline.kind), "label" => outline.label, "metric" => string_key_dict(outline.entry)) - for outline in hasa_outlines - ], - "geometric" => [string_key_dict(stats) for stats in geo_stats], - "hasa" => [string_key_dict(stats) for stats in hasa_stats], - ) -end - -function _voxel_points_3d(mask::AbstractArray{Bool, 3}, grid, cfg::PAMConfig3D) - depth_mm = depth_coordinates_3d(cfg) .* 1e3 - y_mm = collect(grid.y) .* 1e3 - z_mm = collect(grid.z) .* 1e3 - idxs = Tuple.(findall(mask)) - return ( - depth = [depth_mm[idx[1]] for idx in idxs], - y = [y_mm[idx[2]] for idx in idxs], - z = [z_mm[idx[3]] for idx in idxs], - indices = idxs, - ) -end - -function save_best_threshold_volume_3d(path, intensity, grid, cfg, sources; threshold::Real, truth_radius::Real) - local_ref = max(maximum(Float64.(intensity)), eps(Float64)) - pred_mask = intensity .>= Float64(threshold) * local_ref - truth_mask = pam_truth_mask_3d(sources, grid, cfg; radius=truth_radius) - pred = _voxel_points_3d(pred_mask, grid, cfg) - truth = _voxel_points_3d(truth_mask, grid, cfg) + cached_path = joinpath(from_run_dir, "result.jld2") + isfile(cached_path) || error("--from-run-dir must contain result.jld2, missing: $cached_path") + cached = load(cached_path) + c = cached["c"] + rho = haskey(cached, "rho") ? cached["rho"] : fill(Float32(cached["cfg"].rho0), size(c)) + cfg = cached["cfg"] + sources = haskey(cached, "sources") ? cached["sources"] : cached["clusters"] + cached_results = cached["results"] + rf = cached_results[:rf] + medium_info = haskey(cached, "medium_info") ? cached["medium_info"] : Dict{Symbol, Any}(:aberrator => :cached) + bottom_margin_m = nothing + cached_summary_path = joinpath(from_run_dir, "summary.json") + cached_summary = isfile(cached_summary_path) ? JSON3.read(read(cached_summary_path, String)) : nothing + source_variability = source_variability_from_summary(cached_summary) + emission_meta = if !isnothing(cached_summary) && hasproperty(cached_summary, :emission_meta) + Dict{String, Any}(json3_to_any(cached_summary.emission_meta)) + else + Dict{String, Any}( + "source_model" => source_model_from_meta(Dict{String, Any}(), sources) |> String, + "n_emission_sources" => length(sources), + ) + end + emission_meta["from_run_dir"] = abspath(from_run_dir) + source_model = source_model_from_meta(emission_meta, sources) - fig = Figure(size=(1100, 900)) - ax = Axis3( - fig[1, 1]; - title="HASA 3D reconstructed region at best threshold $(round(Float64(threshold); digits=3))", - xlabel="Y [mm]", - ylabel="Z [mm]", - zlabel="Depth [mm]", - aspect=:data, - azimuth=0.75pi, - elevation=0.22pi, - ) + out_dir = if haskey(opts, "out-dir") && !isempty(strip(opts["out-dir"])) + opts["out-dir"] + else + default_reconstruction_output_dir(from_run_dir) + end + mkpath(out_dir) - if !isempty(truth.indices) - scatter!( - ax, - truth.y, - truth.z, - truth.depth; - markersize=10, - color=(:white, 0.16), - strokecolor=(:black, 0.25), - strokewidth=0.4, + recon_frequencies = if haskey(opts, "recon-frequencies-mhz") && !isempty(strip(opts["recon-frequencies-mhz"])) + parse_float_list(opts["recon-frequencies-mhz"]) .* 1e6 + else + default_recon_frequencies(sources) + end + reconstruction_mode = resolve_reconstruction_mode(opts["recon-mode"], source_model) + recon_bandwidth_hz = parse(Float64, opts["recon-bandwidth-khz"]) * 1e3 + window_config = make_window_config(opts, reconstruction_mode) + analysis_mode = parse_analysis_mode(opts["analysis-mode"], source_model) + simulation_info = haskey(cached_results, :simulation) ? cached_results[:simulation] : default_simulation_info(cfg) + truth_centerlines = centerlines_from_emission_meta(emission_meta) + detection_truth_mask = detection_truth_mask_from_meta(emission_meta, pam_grid(cfg), cfg, detection_truth_radius_m) + results = reconstruct_pam_case( + rf, + c, + sources, + cfg; + simulation_info=simulation_info, + frequencies=recon_frequencies, + bandwidth=recon_bandwidth_hz, + use_gpu=parse_bool(opts["recon-use-gpu"]), + reconstruction_axial_step=parse(Float64, opts["recon-step-um"]) * 1e-6, + analysis_mode=analysis_mode, + detection_truth_radius=detection_truth_radius_m, + detection_threshold_ratio=detection_threshold_ratio, + detection_truth_mask=detection_truth_mask, + reconstruction_mode=reconstruction_mode, + window_config=window_config, + show_progress=parse_bool(opts["recon-progress"]), + benchmark=parse_bool(opts["benchmark"]), + window_batch=parse(Int, opts["window-batch"]), ) - end - - if !isempty(pred.indices) - pred_values = [Float64(intensity[idx...]) / local_ref for idx in pred.indices] - sc = scatter!( - ax, - pred.y, - pred.z, - pred.depth; - markersize=14, - color=pred_values, - colormap=:viridis, - colorrange=(Float64(threshold), 1.0), - strokewidth=0, + reconstruction_source = Dict( + "mode" => "cached_rf", + "from_run_dir" => abspath(from_run_dir), + "from_result_jld2" => abspath(cached_path), ) - Colorbar(fig[1, 2], sc; label="Norm. HASA intensity") - else - Label(fig[1, 2], "No voxels at threshold."; tellheight=false) end - truth_sources = source_triples_mm(sources) - scatter!( - ax, - [t[2] for t in truth_sources], - [t[3] for t in truth_sources], - [t[1] for t in truth_sources]; - marker=:xcross, - markersize=24, - color=:red, - strokewidth=2, - ) + medium_summary = TranscranialFUS.run_pam_medium_summary(medium_info) - Label( - fig[2, 1:2], - "Colored voxels are the thresholded HASA reconstruction; translucent white voxels are the truth mask; red x markers are source locations."; - tellwidth=false, - halign=:left, + save_overview( + joinpath(out_dir, "overview.png"), + c, results[:rf], results[:pam_geo], results[:pam_hasa], + results[:kgrid], cfg, sources, results[:stats_geo], results[:stats_hasa], ) - save(path, fig) - return Dict( - "threshold_ratio" => Float64(threshold), - "predicted_voxels" => count(pred_mask), - "truth_voxels" => count(truth_mask), - ) -end - -function save_napari_npz_3d(out_dir, pam_geo, pam_hasa, c, rho, grid, cfg, sources; truth_radius) - np = TranscranialFUS.PythonCall.pyimport("numpy") - - depth_mm = Float32.(depth_coordinates_3d(cfg) .* 1e3) - y_mm = Float32.(collect(grid.y) .* 1e3) - z_mm = Float32.(collect(grid.z) .* 1e3) - - ref = max(maximum(Float64.(pam_hasa)), eps(Float64)) - hasa_norm = Float32.(Float64.(pam_hasa) ./ ref) - geo_norm = Float32.(Float64.(pam_geo) ./ ref) - c_vol = Float32.(c) - rho_vol = Float32.(rho) - truth_mask = Float32.(pam_truth_mask_3d(sources, grid, cfg; radius=truth_radius)) - - triples = source_triples_mm(sources) - src_depth = Float32[t[1] for t in triples] - src_y = Float32[t[2] for t in triples] - src_z = Float32[t[3] for t in triples] - - # voxel spacing in mm for napari scale parameter: (depth, y, z) - scale = [Float64(cfg.dx * 1e3), Float64(cfg.dy * 1e3), Float64(cfg.dz * 1e3)] - - npz_path = joinpath(out_dir, "napari_data.npz") - np.savez( - npz_path, - hasa = hasa_norm, - geometric = geo_norm, - sound_speed = c_vol, - density = rho_vol, - truth_mask = truth_mask, - depth_mm = depth_mm, - y_mm = y_mm, - z_mm = z_mm, - src_depth_mm = src_depth, - src_y_mm = src_y, - src_z_mm = src_z, - scale = Float64.(scale), - ) - - py_script = """ -import numpy as np, napari, sys - -data = np.load(r\"$(replace(npz_path, "\\" => "\\\\"))\") -scale = tuple(data[\"scale\"]) # (depth_mm, y_mm, z_mm) - -viewer = napari.Viewer(title=\"PAM 3D: $(basename(out_dir))\") -viewer.add_image(data[\"hasa\"], name=\"HASA (norm)\", scale=scale, colormap=\"inferno\", opacity=0.9) -viewer.add_image(data[\"geometric\"], name=\"Geometric (norm)\", scale=scale, colormap=\"viridis\", opacity=0.5, visible=False) -viewer.add_image(data[\"sound_speed\"], name=\"Sound speed [m/s]\", scale=scale, colormap=\"gray\", opacity=0.35, visible=False) -viewer.add_image(data[\"density\"], name=\"Density [kg/m3]\", scale=scale, colormap=\"gray\", opacity=0.35, visible=False) -viewer.add_image(data[\"truth_mask\"], name=\"Truth mask\", scale=scale, colormap=\"green\", opacity=0.25) - -depth_idx = np.interp(data[\"src_depth_mm\"], data[\"depth_mm\"], np.arange(len(data[\"depth_mm\"]))) -y_idx = np.interp(data[\"src_y_mm\"], data[\"y_mm\"], np.arange(len(data[\"y_mm\"]))) -z_idx = np.interp(data[\"src_z_mm\"], data[\"z_mm\"], np.arange(len(data[\"z_mm\"]))) -pts = np.stack([depth_idx, y_idx, z_idx], axis=1) -viewer.add_points(pts, name=\"Sources\", size=1.5, face_color=\"red\", symbol=\"cross\", scale=scale) - -napari.run() -""" - open(joinpath(out_dir, "view_pam.py"), "w") do io - write(io, py_script) - end - - println("Saved napari data → $npz_path") - println(" Open with: python $(joinpath(out_dir, "view_pam.py"))") -end - -function compact_window_info(info) - haskey(info, :used_window_count) || return nothing - range_pairs(ranges) = [[first(range), last(range)] for range in ranges] - return Dict( - "total_window_count" => info[:total_window_count], - "used_window_count" => info[:used_window_count], - "skipped_window_count" => info[:skipped_window_count], - "window_samples" => info[:window_samples], - "hop_samples" => info[:hop_samples], - "effective_window_duration_s" => get(info, :effective_window_duration_s, nothing), - "effective_hop_s" => get(info, :effective_hop_s, nothing), - "energy_threshold" => info[:energy_threshold], - "used_window_ranges" => range_pairs(info[:used_window_ranges]), - "skipped_window_ranges" => range_pairs(info[:skipped_window_ranges]), - "accumulation" => haskey(info, :accumulation) ? String(info[:accumulation]) : nothing, - ) -end - -function source_summary(src::PointSource2D) - return Dict( - "kind" => "point", - "depth_m" => src.depth, - "lateral_m" => src.lateral, - "frequency_hz" => src.frequency, - "amplitude_pa" => src.amplitude, - "phase_rad" => src.phase, - "delay_s" => src.delay, - "num_cycles" => src.num_cycles, - ) -end - -function source_summary(src::Union{BubbleCluster2D, GaussianPulseCluster2D}) - return Dict( - "kind" => String(cavitation_model(src)), - "depth_m" => src.depth, - "lateral_m" => src.lateral, - "fundamental_hz" => src.fundamental, - "amplitude_pa" => src.amplitude, - "n_bubbles" => src.n_bubbles, - "harmonics" => src.harmonics, - "harmonic_amplitudes" => src.harmonic_amplitudes, - "harmonic_phases_rad" => src.harmonic_phases, - "cavitation_model" => String(cavitation_model(src)), - "gate_duration_s" => src.gate_duration, - "delay_s" => src.delay, - ) -end - -function source_summary(src::PointSource3D) - return Dict( - "kind" => "point3d", - "depth_m" => src.depth, - "lateral_y_m" => src.lateral_y, - "lateral_z_m" => src.lateral_z, - "frequency_hz" => src.frequency, - "amplitude_pa" => src.amplitude, - "phase_rad" => src.phase, - "delay_s" => src.delay, - "num_cycles" => src.num_cycles, - ) -end - -function source_summary(src::BubbleCluster3D) - return Dict( - "kind" => "bubble3d", - "depth_m" => src.depth, - "lateral_y_m" => src.lateral_y, - "lateral_z_m" => src.lateral_z, - "fundamental_hz" => src.fundamental, - "amplitude_pa" => src.amplitude, - "n_bubbles" => src.n_bubbles, - "harmonics" => src.harmonics, - "harmonic_amplitudes" => src.harmonic_amplitudes, - "harmonic_phases_rad" => src.harmonic_phases, - "gate_duration_s" => src.gate_duration, - "delay_s" => src.delay, - ) -end - -opts, provided_keys = parse_cli(ARGS) -dimension = parse_dimension(opts["dimension"]) -source_model = parse_source_model(opts["source-model"]) -from_run_dir = strip(opts["from-run-dir"]) -peak_method = Symbol(lowercase(strip(opts["peak-method"]))) -peak_method in (:argmax, :clean) || error("--peak-method must be argmax or clean, got: $(opts["peak-method"])") -detection_truth_radius_m = parse(Float64, opts["vascular-radius-mm"]) * 1e-3 -detection_threshold_ratio = parse(Float64, opts["detection-threshold-ratio"]) -boundary_threshold_ratios = parse_threshold_ratios(opts["boundary-threshold-ratios"]) -auto_threshold_search = parse_bool(opts["auto-threshold-search"]) -threshold_score_ratios = auto_threshold_search ? parse_threshold_search_ratios(opts) : boundary_threshold_ratios - -if dimension == 3 - isempty(from_run_dir) || error("--from-run-dir is not implemented for 3D PAM yet.") - source_model in (:point, :squiggle, :network) || - error("3D PAM CLI supports --source-model=point, --source-model=squiggle, or --source-model=network.") - aberrator = parse_aberrator(opts["aberrator"]) - aberrator in (:none, :skull) || error("3D PAM CLI currently supports only --aberrator=none or --aberrator=skull.") - - dy_mm = isempty(strip(opts["dy-mm"])) ? parse(Float64, opts["dz-mm"]) : parse(Float64, opts["dy-mm"]) - transverse_y_mm = isempty(strip(opts["transverse-y-mm"])) ? parse(Float64, opts["transverse-mm"]) : parse(Float64, opts["transverse-y-mm"]) - transverse_z_mm = isempty(strip(opts["transverse-z-mm"])) ? parse(Float64, opts["transverse-mm"]) : parse(Float64, opts["transverse-z-mm"]) - receiver_aperture_y_spec = isempty(strip(opts["receiver-aperture-y-mm"])) ? opts["receiver-aperture-mm"] : opts["receiver-aperture-y-mm"] - receiver_aperture_z_spec = isempty(strip(opts["receiver-aperture-z-mm"])) ? opts["receiver-aperture-mm"] : opts["receiver-aperture-z-mm"] - - cfg_base = PAMConfig3D( - dx=parse(Float64, opts["dx-mm"]) * 1e-3, - dy=dy_mm * 1e-3, - dz=parse(Float64, opts["dz-mm"]) * 1e-3, - axial_dim=parse(Float64, opts["axial-mm"]) * 1e-3, - transverse_dim_y=transverse_y_mm * 1e-3, - transverse_dim_z=transverse_z_mm * 1e-3, - t_max=parse(Float64, opts["t-max-us"]) * 1e-6, - dt=parse(Float64, opts["dt-ns"]) * 1e-9, - zero_pad_factor=parse(Int, opts["zero-pad-factor"]), - receiver_aperture_y=parse_receiver_aperture_mm(receiver_aperture_y_spec), - receiver_aperture_z=parse_receiver_aperture_mm(receiver_aperture_z_spec), - peak_suppression_radius=parse(Float64, opts["peak-suppression-radius-mm"]) * 1e-3, - success_tolerance=parse(Float64, opts["success-tolerance-mm"]) * 1e-3, - axial_gain_power=parse(Float64, opts["axial-gain-power"]), - ) - - sources, emission_meta = if source_model == :point - parse_point_sources_3d(opts) - elseif source_model == :network - parse_network_sources_3d(opts, cfg_base) - else - parse_squiggle_sources_3d(opts, cfg_base) - end - bottom_margin_m = parse(Float64, opts["bottom-margin-mm"]) * 1e-3 - cfg = fit_pam_config_3d(cfg_base, sources; min_bottom_margin=bottom_margin_m) - - out_dir = if haskey(opts, "out-dir") && !isempty(strip(opts["out-dir"])) - opts["out-dir"] - else - default_output_dir(opts, sources, cfg, emission_meta) - end - mkpath(out_dir) - - c, rho, medium_info = make_pam_medium_3d(cfg; - aberrator = aberrator, - ct_path = opts["ct-path"], - slice_index_z = parse(Int, opts["slice-index"]), - skull_to_transducer = parse(Float64, opts["skull-transducer-distance-mm"]) * 1e-3, - hu_bone_thr = parse(Int, opts["hu-bone-thr"]), - ) - recon_frequencies = if haskey(opts, "recon-frequencies-mhz") && !isempty(strip(opts["recon-frequencies-mhz"])) - parse_float_list(opts["recon-frequencies-mhz"]) .* 1e6 - else - default_recon_frequencies(sources) - end - reconstruction_mode = resolve_reconstruction_mode(opts["recon-mode"], source_model) - recon_bandwidth_hz = parse(Float64, opts["recon-bandwidth-khz"]) * 1e3 - window_config = make_window_config(opts, reconstruction_mode) - source_phase_mode = parse_source_phase_mode(opts["source-phase-mode"]) - rng_sim = Random.MersenneTwister(parse(Int, opts["random-seed"]) + 1) - source_variability = parse_source_variability(opts) - if source_model in (:squiggle, :network) - emission_meta["activity_model"] = Dict( - "activity_mode" => String(source_phase_mode), - "frequency_jitter_percent" => source_variability.frequency_jitter_fraction * 100.0, - ) - end - - sim_mode = parse_sim_mode(opts["sim-mode"]) - sim_mode == :analytic && aberrator == :skull && error("--sim-mode=analytic is not compatible with --aberrator=skull; use --sim-mode=kwave.") - results = run_pam_case_3d( - c, - rho, - sources, - cfg; - frequencies=recon_frequencies, - bandwidth=recon_bandwidth_hz, - use_gpu=parse_bool(opts["use-gpu"]), - reconstruction_axial_step=parse(Float64, opts["recon-step-um"]) * 1e-6, - reconstruction_mode=reconstruction_mode, - window_config=window_config, - show_progress=parse_bool(opts["recon-progress"]), - benchmark=parse_bool(opts["benchmark"]), - window_batch=parse(Int, opts["window-batch"]), - sim_mode=sim_mode, - source_phase_mode=source_phase_mode, - rng=rng_sim, - source_variability=source_variability, - ) - - medium_summary = Dict{String, Any}() - for (key, value) in medium_info - key == :mask && continue - medium_summary[String(key)] = value - end activity_boundary_path = joinpath(out_dir, "activity_boundaries.png") - activity_boundary_metrics = save_threshold_boundary_detection_3d( + activity_boundary_metrics = save_threshold_boundary_detection( activity_boundary_path, results[:pam_geo], results[:pam_hasa], results[:kgrid], cfg, sources; - threshold_ratios=threshold_score_ratios, + threshold_ratios=boundary_threshold_ratios, truth_radius=detection_truth_radius_m, + truth_mask=detection_truth_mask, + truth_centerlines=truth_centerlines, + frequencies=recon_frequencies, c=c, ) - activity_boundary_metrics["auto_threshold_search"] = auto_threshold_search - activity_boundary_metrics["display_threshold_mode"] = "selected_best_recall_precision" - best_volume_path = joinpath(out_dir, "best_threshold_3d.png") - best_volume_metrics = save_best_threshold_volume_3d( - best_volume_path, - results[:pam_hasa], - results[:kgrid], - cfg, - sources; - threshold=activity_boundary_metrics["best_hasa_threshold"], - truth_radius=detection_truth_radius_m, - ) summary = Dict( "out_dir" => out_dir, - "dimension" => 3, - "reconstruction_source" => Dict("mode" => aberrator == :none ? "analytic_3d_water" : "heterogeneous_3d"), + "reconstruction_source" => reconstruction_source, "activity_boundary_figure" => activity_boundary_path, "activity_boundary_metrics" => activity_boundary_metrics, - "best_threshold_3d_figure" => best_volume_path, - "best_threshold_3d_metrics" => best_volume_metrics, "sources" => [source_summary(src) for src in sources], - "clusters" => [source_summary(src) for src in sources], "emission_meta" => emission_meta, "config" => Dict( "dx" => cfg.dx, - "dy" => cfg.dy, "dz" => cfg.dz, "axial_dim" => cfg.axial_dim, - "transverse_dim_y" => cfg.transverse_dim_y, - "transverse_dim_z" => cfg.transverse_dim_z, - "receiver_aperture_y" => cfg.receiver_aperture_y, - "receiver_aperture_z" => cfg.receiver_aperture_z, + "transverse_dim" => cfg.transverse_dim, + "receiver_aperture" => cfg.receiver_aperture, "t_max" => cfg.t_max, "dt" => cfg.dt, "c0" => cfg.c0, @@ -2096,7 +482,6 @@ if dimension == 3 "zero_pad_factor" => cfg.zero_pad_factor, "peak_suppression_radius" => cfg.peak_suppression_radius, "success_tolerance" => cfg.success_tolerance, - "axial_gain_power" => cfg.axial_gain_power, "bottom_margin" => bottom_margin_m, ), "medium" => medium_summary, @@ -2104,22 +489,10 @@ if dimension == 3 "reconstruction_bandwidth_hz" => recon_bandwidth_hz, "reconstruction_mode" => String(results[:reconstruction_mode]), "reconstruction_progress" => parse_bool(opts["recon-progress"]), - "source_phase_mode" => String(results[:source_phase_mode]), - "n_frames" => Int(get(results, :n_frames, 1)), + "source_phase_mode" => String(get(results, :source_phase_mode, :coherent)), "source_variability" => Dict( "frequency_jitter_percent" => source_variability.frequency_jitter_fraction * 100.0, ), - "threshold_search" => Dict( - "auto" => auto_threshold_search, - "min_ratio" => minimum(threshold_score_ratios), - "max_ratio" => maximum(threshold_score_ratios), - "step" => auto_threshold_search ? parse(Float64, opts["auto-threshold-step"]) : nothing, - "count" => length(threshold_score_ratios), - "selection_metric" => "source_f1", - "display_threshold_mode" => "selected_best_recall_precision", - ), - "physical_source_count" => length(sources), - "emission_event_count" => Int(get(results, :emission_event_count, length(sources))), "window_config" => string_key_dict(results[:window_config]), "window_info" => Dict( "geometric" => compact_window_info(results[:geo_info]), @@ -2132,12 +505,18 @@ if dimension == 3 ), "reconstruction_axial_step_m" => results[:geo_info][:axial_step], "reference_sound_speed_m_per_s" => results[:geo_info][:reference_sound_speed], - "analysis_mode" => String(results[:analysis_mode]), + "activity_model" => get(emission_meta, "activity_model", Dict("activity_mode" => "static")), + "physical_source_count" => get(emission_meta, "physical_source_count", length(sources)), + "emission_event_count" => get(emission_meta, "emission_event_count", length(sources)), + "analysis_mode" => String(analysis_mode), + "detection_truth_radius_m" => detection_truth_radius_m, + "detection_truth_mode" => isnothing(detection_truth_mask) ? "source_disks" : "centerline_tube", + "detection_threshold_ratio" => detection_threshold_ratio, + "boundary_threshold_ratios" => boundary_threshold_ratios, "simulation" => Dict( "receiver_row" => results[:simulation][:receiver_row], - "receiver_cols_y" => [first(results[:simulation][:receiver_cols_y]), last(results[:simulation][:receiver_cols_y])], - "receiver_cols_z" => [first(results[:simulation][:receiver_cols_z]), last(results[:simulation][:receiver_cols_z])], - "source_indices" => [[row, col_y, col_z] for (row, col_y, col_z) in get(results[:simulation], :source_indices, NTuple{3, Int}[])], + "receiver_cols" => [first(results[:simulation][:receiver_cols]), last(results[:simulation][:receiver_cols])], + "source_indices" => [[row, col] for (row, col) in get(results[:simulation], :source_indices, Tuple{Int, Int}[])], ), "geometric" => results[:stats_geo], "hasa" => results[:stats_hasa], @@ -2147,324 +526,13 @@ if dimension == 3 JSON3.pretty(io, summary) end - clusters = sources - @save joinpath(out_dir, "result.jld2") c rho cfg clusters results medium_info - - save_napari_npz_3d( - out_dir, - results[:pam_geo], - results[:pam_hasa], - c, rho, - results[:kgrid], - cfg, - sources; - truth_radius=detection_truth_radius_m, - ) - - println("Saved 3D PAM outputs to $out_dir") - exit() -end - -if isempty(from_run_dir) - cfg_base = PAMConfig( - dx=parse(Float64, opts["dx-mm"]) * 1e-3, - dz=parse(Float64, opts["dz-mm"]) * 1e-3, - axial_dim=parse(Float64, opts["axial-mm"]) * 1e-3, - transverse_dim=parse(Float64, opts["transverse-mm"]) * 1e-3, - receiver_aperture=parse_receiver_aperture_mm(opts["receiver-aperture-mm"]), - t_max=parse(Float64, opts["t-max-us"]) * 1e-6, - dt=parse(Float64, opts["dt-ns"]) * 1e-9, - zero_pad_factor=parse(Int, opts["zero-pad-factor"]), - peak_suppression_radius=parse(Float64, opts["peak-suppression-radius-mm"]) * 1e-3, - success_tolerance=parse(Float64, opts["success-tolerance-mm"]) * 1e-3, - ) - - sources, emission_meta = parse_sources(opts, cfg_base) - source_model = source_model_from_meta(emission_meta, sources) - - aberrator = parse_aberrator(opts["aberrator"]) - bottom_margin_m = parse(Float64, opts["bottom-margin-mm"]) * 1e-3 - cfg = fit_pam_config( - cfg_base, - sources; - min_bottom_margin=bottom_margin_m, - reference_depth=aberrator == :skull ? parse(Float64, opts["skull-transducer-distance-mm"]) * 1e-3 : nothing, - ) - - out_dir = if haskey(opts, "out-dir") && !isempty(strip(opts["out-dir"])) - opts["out-dir"] - else - default_output_dir(opts, sources, cfg, emission_meta) - end - mkpath(out_dir) - - c, rho, medium_info = make_pam_medium( - cfg; - aberrator=aberrator, - lens_center_depth=parse(Float64, opts["lens-depth-mm"]) * 1e-3, - lens_center_lateral=parse(Float64, opts["lens-lateral-mm"]) * 1e-3, - lens_axial_radius=parse(Float64, opts["lens-axial-radius-mm"]) * 1e-3, - lens_lateral_radius=parse(Float64, opts["lens-lateral-radius-mm"]) * 1e-3, - c_aberrator=parse(Float64, opts["aberrator-c"]), - rho_aberrator=parse(Float64, opts["aberrator-rho"]), - ct_path=opts["ct-path"], - slice_index=parse(Int, opts["slice-index"]), - skull_to_transducer=parse(Float64, opts["skull-transducer-distance-mm"]) * 1e-3, - hu_bone_thr=parse(Int, opts["hu-bone-thr"]), - ) - - recon_frequencies = if haskey(opts, "recon-frequencies-mhz") && !isempty(strip(opts["recon-frequencies-mhz"])) - parse_float_list(opts["recon-frequencies-mhz"]) .* 1e6 - else - default_recon_frequencies(sources) - end - reconstruction_mode = resolve_reconstruction_mode(opts["recon-mode"], source_model) - recon_bandwidth_hz = parse(Float64, opts["recon-bandwidth-khz"]) * 1e3 - window_config = make_window_config(opts, reconstruction_mode) - analysis_mode = parse_analysis_mode(opts["analysis-mode"], source_model) - truth_centerlines = centerlines_from_emission_meta(emission_meta) - detection_truth_mask = detection_truth_mask_from_meta(emission_meta, pam_grid(cfg), cfg, detection_truth_radius_m) - - source_phase_mode = parse_source_phase_mode(opts["source-phase-mode"]) - n_realizations = parse(Int, opts["n-realizations"]) - rng_sim = Random.MersenneTwister(parse(Int, opts["random-seed"]) + 1) - source_variability = parse_source_variability(opts) - if source_model == :squiggle - emission_meta["activity_model"] = Dict( - "activity_mode" => String(source_phase_mode), - "frequency_jitter_percent" => source_variability.frequency_jitter_fraction * 100.0, - ) - end + @save joinpath(out_dir, "result.jld2") c rho cfg sources results medium_info - results = run_pam_case( - c, - rho, - sources, - cfg; - frequencies=recon_frequencies, - bandwidth=recon_bandwidth_hz, - use_gpu=parse_bool(opts["use-gpu"]), - reconstruction_axial_step=parse(Float64, opts["recon-step-um"]) * 1e-6, - analysis_mode=analysis_mode, - peak_method=peak_method, - clean_loop_gain=parse(Float64, opts["clean-loop-gain"]), - clean_max_iter=parse(Int, opts["clean-max-iter"]), - clean_threshold_ratio=parse(Float64, opts["clean-threshold-ratio"]), - detection_truth_radius=detection_truth_radius_m, - detection_threshold_ratio=detection_threshold_ratio, - detection_truth_mask=detection_truth_mask, - reconstruction_mode=reconstruction_mode, - window_config=window_config, - source_phase_mode=source_phase_mode, - n_realizations=n_realizations, - rng=rng_sim, - source_variability=source_variability, - show_progress=parse_bool(opts["recon-progress"]), - benchmark=parse_bool(opts["benchmark"]), - window_batch=parse(Int, opts["window-batch"]), - ) - reconstruction_source = Dict("mode" => "simulation") -else - reject_cached_simulation_options!( - provided_keys, - ( - "source-model", "sources-mm", "anchors-mm", "frequency-mhz", "fundamental-mhz", - "amplitude-pa", "source-amplitudes-pa", "source-frequencies-mhz", "phases-deg", - "n-bubbles", "num-cycles", "harmonics", "harmonic-amplitudes", "cavitation-model", - "gate-us", "taper-ratio", "axial-mm", "transverse-mm", "dx-mm", "dz-mm", - "receiver-aperture-mm", "t-max-us", "dt-ns", "zero-pad-factor", - "peak-suppression-radius-mm", "success-tolerance-mm", "aberrator", "ct-path", - "slice-index", "skull-transducer-distance-mm", "bottom-margin-mm", "hu-bone-thr", - "lens-depth-mm", "lens-lateral-mm", "lens-axial-radius-mm", "lens-lateral-radius-mm", - "aberrator-c", "aberrator-rho", "phase-mode", "phase-jitter-rad", "random-seed", - "transducer-mm", "delays-us", "vascular-length-mm", "vascular-squiggle-amplitude-mm", - "vascular-squiggle-amplitude-x-mm", "vascular-squiggle-wavelength-mm", - "vascular-squiggle-slope", "squiggle-phase-x-deg", - "vascular-source-spacing-mm", "vascular-position-jitter-mm", - "vascular-min-separation-mm", "vascular-max-sources-per-anchor", - "network-radius-mm", "network-axial-radius-mm", "network-lateral-y-radius-mm", - "network-lateral-z-radius-mm", "network-root-count", "network-generations", - "network-branch-length-mm", "network-branch-step-mm", "network-branch-angle-deg", - "network-tortuosity", "network-orientation", "network-density-sigma-mm", "network-density-axial-sigma-mm", - "network-density-lateral-y-sigma-mm", "network-density-lateral-z-sigma-mm", - "network-max-sources-per-center", - "source-phase-mode", "n-realizations", "frequency-jitter-percent", - ), - ) - cached_path = joinpath(from_run_dir, "result.jld2") - isfile(cached_path) || error("--from-run-dir must contain result.jld2, missing: $cached_path") - cached = load(cached_path) - c = cached["c"] - rho = haskey(cached, "rho") ? cached["rho"] : fill(Float32(cached["cfg"].rho0), size(c)) - cfg = cached["cfg"] - sources = cached["clusters"] - cached_results = cached["results"] - rf = cached_results[:rf] - medium_info = haskey(cached, "medium_info") ? cached["medium_info"] : Dict{Symbol, Any}(:aberrator => :cached) - bottom_margin_m = nothing - cached_summary_path = joinpath(from_run_dir, "summary.json") - cached_summary = isfile(cached_summary_path) ? JSON3.read(read(cached_summary_path, String)) : nothing - source_variability = source_variability_from_summary(cached_summary) - emission_meta = if !isnothing(cached_summary) && hasproperty(cached_summary, :emission_meta) - Dict{String, Any}(json3_to_any(cached_summary.emission_meta)) - else - Dict{String, Any}( - "source_model" => source_model_from_meta(Dict{String, Any}(), sources) |> String, - "n_emission_sources" => length(sources), - ) - end - emission_meta["from_run_dir"] = abspath(from_run_dir) - source_model = source_model_from_meta(emission_meta, sources) + println("Saved PAM outputs to $out_dir") - out_dir = if haskey(opts, "out-dir") && !isempty(strip(opts["out-dir"])) - opts["out-dir"] - else - default_reconstruction_output_dir(from_run_dir) - end - mkpath(out_dir) - - recon_frequencies = if haskey(opts, "recon-frequencies-mhz") && !isempty(strip(opts["recon-frequencies-mhz"])) - parse_float_list(opts["recon-frequencies-mhz"]) .* 1e6 - else - default_recon_frequencies(sources) - end - reconstruction_mode = resolve_reconstruction_mode(opts["recon-mode"], source_model) - recon_bandwidth_hz = parse(Float64, opts["recon-bandwidth-khz"]) * 1e3 - window_config = make_window_config(opts, reconstruction_mode) - analysis_mode = parse_analysis_mode(opts["analysis-mode"], source_model) - simulation_info = haskey(cached_results, :simulation) ? cached_results[:simulation] : default_simulation_info(cfg) - truth_centerlines = centerlines_from_emission_meta(emission_meta) - detection_truth_mask = detection_truth_mask_from_meta(emission_meta, pam_grid(cfg), cfg, detection_truth_radius_m) - results = reconstruct_pam_case( - rf, - c, - sources, - cfg; - simulation_info=simulation_info, - frequencies=recon_frequencies, - bandwidth=recon_bandwidth_hz, - use_gpu=parse_bool(opts["use-gpu"]), - reconstruction_axial_step=parse(Float64, opts["recon-step-um"]) * 1e-6, - analysis_mode=analysis_mode, - peak_method=peak_method, - clean_loop_gain=parse(Float64, opts["clean-loop-gain"]), - clean_max_iter=parse(Int, opts["clean-max-iter"]), - clean_threshold_ratio=parse(Float64, opts["clean-threshold-ratio"]), - detection_truth_radius=detection_truth_radius_m, - detection_threshold_ratio=detection_threshold_ratio, - detection_truth_mask=detection_truth_mask, - reconstruction_mode=reconstruction_mode, - window_config=window_config, - show_progress=parse_bool(opts["recon-progress"]), - benchmark=parse_bool(opts["benchmark"]), - window_batch=parse(Int, opts["window-batch"]), - ) - reconstruction_source = Dict( - "mode" => "cached_rf", - "from_run_dir" => abspath(from_run_dir), - "from_result_jld2" => abspath(cached_path), - ) + return 0 end -medium_summary = Dict{String, Any}() -for (key, value) in medium_info - key == :mask && continue - medium_summary[String(key)] = value +if abspath(PROGRAM_FILE) == @__FILE__ + exit(main(ARGS)) end - -save_overview( - joinpath(out_dir, "overview.png"), - c, results[:rf], results[:pam_geo], results[:pam_hasa], - results[:kgrid], cfg, sources, results[:stats_geo], results[:stats_hasa], -) - -activity_boundary_path = joinpath(out_dir, "activity_boundaries.png") -activity_boundary_metrics = save_threshold_boundary_detection( - activity_boundary_path, - results[:pam_geo], - results[:pam_hasa], - results[:kgrid], - cfg, - sources; - threshold_ratios=boundary_threshold_ratios, - truth_radius=detection_truth_radius_m, - truth_mask=detection_truth_mask, - truth_centerlines=truth_centerlines, - frequencies=recon_frequencies, - c=c, -) - -summary = Dict( - "out_dir" => out_dir, - "reconstruction_source" => reconstruction_source, - "activity_boundary_figure" => activity_boundary_path, - "activity_boundary_metrics" => activity_boundary_metrics, - "sources" => [source_summary(src) for src in sources], - "clusters" => [source_summary(src) for src in sources], - "emission_meta" => emission_meta, - "config" => Dict( - "dx" => cfg.dx, - "dz" => cfg.dz, - "axial_dim" => cfg.axial_dim, - "transverse_dim" => cfg.transverse_dim, - "receiver_aperture" => cfg.receiver_aperture, - "t_max" => cfg.t_max, - "dt" => cfg.dt, - "c0" => cfg.c0, - "rho0" => cfg.rho0, - "zero_pad_factor" => cfg.zero_pad_factor, - "peak_suppression_radius" => cfg.peak_suppression_radius, - "success_tolerance" => cfg.success_tolerance, - "bottom_margin" => bottom_margin_m, - ), - "medium" => medium_summary, - "reconstruction_frequencies_hz" => recon_frequencies, - "reconstruction_bandwidth_hz" => recon_bandwidth_hz, - "reconstruction_mode" => String(results[:reconstruction_mode]), - "reconstruction_progress" => parse_bool(opts["recon-progress"]), - "source_phase_mode" => String(get(results, :source_phase_mode, :coherent)), - "n_realizations" => Int(get(results, :n_realizations, 1)), - "source_variability" => Dict( - "frequency_jitter_percent" => source_variability.frequency_jitter_fraction * 100.0, - ), - "window_config" => string_key_dict(results[:window_config]), - "window_info" => Dict( - "geometric" => compact_window_info(results[:geo_info]), - "hasa" => compact_window_info(results[:hasa_info]), - ), - "benchmark" => parse_bool(opts["benchmark"]), - "gpu_timing" => Dict( - "geometric" => get(results[:geo_info], :gpu_timing, nothing), - "hasa" => get(results[:hasa_info], :gpu_timing, nothing), - ), - "reconstruction_axial_step_m" => results[:geo_info][:axial_step], - "reference_sound_speed_m_per_s" => results[:geo_info][:reference_sound_speed], - "activity_model" => get(emission_meta, "activity_model", Dict("activity_mode" => "static")), - "physical_source_count" => get(emission_meta, "physical_source_count", length(sources)), - "emission_event_count" => get(emission_meta, "emission_event_count", length(sources)), - "analysis_mode" => String(analysis_mode), - "detection_truth_radius_m" => detection_truth_radius_m, - "detection_truth_mode" => isnothing(detection_truth_mask) ? "source_disks" : "centerline_tube", - "detection_threshold_ratio" => detection_threshold_ratio, - "boundary_threshold_ratios" => boundary_threshold_ratios, - "peak_method" => String(peak_method), - "clean_loop_gain" => parse(Float64, opts["clean-loop-gain"]), - "clean_max_iter" => parse(Int, opts["clean-max-iter"]), - "clean_threshold_ratio" => parse(Float64, opts["clean-threshold-ratio"]), - "simulation" => Dict( - "receiver_row" => results[:simulation][:receiver_row], - "receiver_cols" => [first(results[:simulation][:receiver_cols]), last(results[:simulation][:receiver_cols])], - "source_indices" => [[row, col] for (row, col) in get(results[:simulation], :source_indices, Tuple{Int, Int}[])], - ), - "geometric" => results[:stats_geo], - "hasa" => results[:stats_hasa], -) - -open(joinpath(out_dir, "summary.json"), "w") do io - JSON3.pretty(io, summary) -end - -clusters = sources -@save joinpath(out_dir, "result.jld2") c rho cfg clusters results medium_info - -println("Saved PAM outputs to $out_dir") diff --git a/scripts/run_pam_3d_focused_sweep.jl b/scripts/run_pam_3d_focused_sweep.jl deleted file mode 100644 index c8175d6..0000000 --- a/scripts/run_pam_3d_focused_sweep.jl +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env julia - -include(joinpath(@__DIR__, "run_pam_overnight_sweep.jl")) - -function focused_sweep_opts(args) - opts = parse_cli(args) - provided = Set(first(split(arg[3:end], "="; limit=2)) for arg in args if startswith(arg, "--")) - !("max-hours" in provided) && (opts["max-hours"] = "0.75") - !("per-run-timeout-min" in provided) && (opts["per-run-timeout-min"] = "8") - !("output-root" in provided) && (opts["output-root"] = "") - !("random-seed" in provided) && (opts["random-seed"] = "42") - !("boundary-threshold-ratios" in provided) && - (opts["boundary-threshold-ratios"] = "0.45,0.5,0.55,0.6,0.65,0.7,0.75,0.8,0.85,0.9") - !("auto-threshold-search" in provided) && (opts["auto-threshold-search"] = "true") - !("auto-threshold-min" in provided) && (opts["auto-threshold-min"] = "0.10") - !("auto-threshold-max" in provided) && (opts["auto-threshold-max"] = "0.95") - !("auto-threshold-step" in provided) && (opts["auto-threshold-step"] = "0.01") - return opts -end - -function focused_base_args(opts) - return Dict( - "dimension" => "3", - "source-model" => "squiggle", - "gate-us" => "45", - "anchors-mm" => "42:0:0", - "vascular-length-mm" => "12", - "vascular-squiggle-amplitude-mm" => "0.3", - "vascular-squiggle-amplitude-x-mm" => "0.2", - "vascular-squiggle-wavelength-mm" => "8", - "squiggle-phase-x-deg" => "90", - "vascular-source-spacing-mm" => "0.5", - "vascular-min-separation-mm" => "0.25", - "harmonics" => "2,3,4", - "harmonic-amplitudes" => "1.0,0.6,0.3", - "aberrator" => "skull", - "skull-transducer-distance-mm" => "20", - "slice-index" => opts["slice-index"], - "axial-mm" => "70", - "transverse-mm" => "64", - "dx-mm" => "0.2", - "dy-mm" => "0.5", - "dz-mm" => "0.5", - "t-max-us" => "250", - "frequency-mhz" => "0.5", - "receiver-aperture-mm" => "full", - "source-phase-mode" => "random_phase_per_window", - "recon-window-us" => "40", - "recon-hop-us" => "20", - "recon-bandwidth-khz" => "500", - "boundary-threshold-ratios" => opts["boundary-threshold-ratios"], - "auto-threshold-search" => opts["auto-threshold-search"], - "auto-threshold-min" => opts["auto-threshold-min"], - "auto-threshold-max" => opts["auto-threshold-max"], - "auto-threshold-step" => opts["auto-threshold-step"], - "frequency-jitter-percent" => "1", - "axial-gain-power" => "1.5", - "sim-mode" => "kwave", - "use-gpu" => "true", - "window-batch" => "2", - "recon-progress" => "false", - "random-seed" => opts["random-seed"], - ) -end - -function focused_sweep_jobs(opts) - base = focused_base_args(opts) - specs = [ - ("bw400", Dict("recon-bandwidth-khz" => "400"), "Narrower bandwidth: fewer FFT bins, possible artifact reduction."), - ("bw300", Dict("recon-bandwidth-khz" => "300"), "Aggressive bandwidth trim for speed and elongated-artifact check."), - ("axgain075_bw400", Dict("recon-bandwidth-khz" => "400", "axial-gain-power" => "0.75"), "Lower axial gain may reduce stretched deep artifacts."), - ("axgain000_bw400", Dict("recon-bandwidth-khz" => "400", "axial-gain-power" => "0.0"), "No axial gain: artifact/precision diagnostic."), - ("fjitter3_bw400", Dict("recon-bandwidth-khz" => "400", "frequency-jitter-percent" => "3"), "More source variability to decorrelate off-source interference."), - ("fjitter5_bw400", Dict("recon-bandwidth-khz" => "400", "frequency-jitter-percent" => "5"), "High source variability stress test."), - ("w30_bw400", Dict("recon-window-us" => "30", "recon-hop-us" => "15", "recon-bandwidth-khz" => "400"), "Shorter windows: more phase realizations, tighter time support."), - ("w50_bw400", Dict("gate-us" => "50", "recon-window-us" => "50", "recon-hop-us" => "25", "recon-bandwidth-khz" => "400"), "Longer windows: fewer FFT batches and more per-window energy."), - ("dense03_bw400", Dict("vascular-source-spacing-mm" => "0.3", "vascular-min-separation-mm" => "0.15", "recon-bandwidth-khz" => "400"), "More realistic denser bubble sampling, moderate source count."), - ("batch4_bw400", Dict("recon-bandwidth-khz" => "400", "window-batch" => "4"), "Runtime-only check: larger window batch after accumulator memory fix."), - ] - return [sim_job(id, merge_args(base, args); note=note) for (id, args, note) in specs] -end - -function focused_sweep_main() - opts = focused_sweep_opts(ARGS) - dry_run = parse_bool(opts["dry-run"]) - force = parse_bool(opts["force"]) - max_seconds = parse(Float64, opts["max-hours"]) * 3600 - timeout_seconds = parse(Float64, opts["per-run-timeout-min"]) * 60 - out_root = isempty(strip(opts["output-root"])) ? - joinpath(PROJECT_ROOT, "outputs", "$(timestamp())_pam_3d_focused_sweep") : - abspath(opts["output-root"]) - mkpath(out_root) - - jobs = focused_sweep_jobs(opts) - write_json(joinpath(out_root, "manifest.json"), Dict{String, Any}( - "created_at" => string(Dates.now()), - "output_root" => out_root, - "max_hours" => parse(Float64, opts["max-hours"]), - "per_run_timeout_min" => parse(Float64, opts["per-run-timeout-min"]), - "job_count" => length(jobs), - "selection_metric" => "source_f1", - "jobs" => jobs, - )) - - println("Output root: ", out_root) - println("Jobs queued: ", length(jobs)) - println("Max hours: ", opts["max-hours"], " | per-run timeout min: ", opts["per-run-timeout-min"]) - dry_run && println("Dry run only; no jobs will be executed.") - - rows = Dict{String, Any}[] - csv_path = joinpath(out_root, "results.csv") - start_all = time() - for (idx, job) in pairs(jobs) - remaining_seconds = max_seconds - (time() - start_all) - if !dry_run && remaining_seconds <= 0 - println("Time budget exhausted before job ", idx, "; stopping.") - break - end - job_timeout_seconds = dry_run ? timeout_seconds : min(timeout_seconds, remaining_seconds) - row = run_job(job, out_root, idx, length(jobs), job_timeout_seconds, dry_run, force) - push!(rows, row) - append_csv(csv_path, row) - write_json(joinpath(out_root, "results.json"), rows) - write_json(joinpath(out_root, "leaderboard.json"), leaderboard_rows(rows)) - end - - leaders = leaderboard_rows(rows) - println() - println("Focused 3D PAM sweep complete. Results: ", out_root) - if isempty(leaders) - println("No successful jobs with activity-boundary metrics yet.") - else - println("Top HASA source-F1:") - for row in first(leaders, min(10, length(leaders))) - @printf(" %.3f thr=%.2f prec=%.3f rec=%.3f %s\n", - Float64(row["best_hasa_f1"]), - Float64(row["best_hasa_threshold"]), - Float64(get(row, "best_hasa_precision", NaN)), - Float64(get(row, "best_hasa_recall", NaN)), - row["id"], - ) - end - end -end - -if abspath(PROGRAM_FILE) == abspath(@__FILE__) - focused_sweep_main() -end diff --git a/scripts/run_pam_3d_jitter_bandwidth_sweep.jl b/scripts/run_pam_3d_jitter_bandwidth_sweep.jl deleted file mode 100644 index dd41718..0000000 --- a/scripts/run_pam_3d_jitter_bandwidth_sweep.jl +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env julia - -include(joinpath(@__DIR__, "run_pam_overnight_sweep.jl")) - -function jitter_bandwidth_opts(args) - opts = parse_cli(args) - provided = Set(first(split(arg[3:end], "="; limit=2)) for arg in args if startswith(arg, "--")) - !("max-hours" in provided) && (opts["max-hours"] = "0.75") - !("per-run-timeout-min" in provided) && (opts["per-run-timeout-min"] = "12") - !("output-root" in provided) && (opts["output-root"] = "") - !("random-seed" in provided) && (opts["random-seed"] = "42") - !("auto-threshold-search" in provided) && (opts["auto-threshold-search"] = "true") - !("auto-threshold-min" in provided) && (opts["auto-threshold-min"] = "0.10") - !("auto-threshold-max" in provided) && (opts["auto-threshold-max"] = "0.95") - !("auto-threshold-step" in provided) && (opts["auto-threshold-step"] = "0.01") - return opts -end - -function jitter_bandwidth_base_args(opts) - return Dict( - "dimension" => "3", - "source-model" => "squiggle", - "gate-us" => "45", - "anchors-mm" => "42:0:0", - "vascular-length-mm" => "12", - "vascular-squiggle-amplitude-mm" => "0.3", - "vascular-squiggle-amplitude-x-mm" => "0.2", - "vascular-squiggle-wavelength-mm" => "8", - "squiggle-phase-x-deg" => "90", - "vascular-source-spacing-mm" => "0.5", - "vascular-min-separation-mm" => "0.25", - "harmonics" => "2,3,4", - "harmonic-amplitudes" => "1.0,0.6,0.3", - "aberrator" => "skull", - "skull-transducer-distance-mm" => "20", - "slice-index" => opts["slice-index"], - "axial-mm" => "70", - "transverse-mm" => "64", - "dx-mm" => "0.2", - "dy-mm" => "0.5", - "dz-mm" => "0.5", - "t-max-us" => "250", - "frequency-mhz" => "0.5", - "receiver-aperture-mm" => "full", - "source-phase-mode" => "random_phase_per_window", - "frequency-jitter-percent" => "10", - "recon-window-us" => "40", - "recon-hop-us" => "20", - "auto-threshold-search" => opts["auto-threshold-search"], - "auto-threshold-min" => opts["auto-threshold-min"], - "auto-threshold-max" => opts["auto-threshold-max"], - "auto-threshold-step" => opts["auto-threshold-step"], - "sim-mode" => "kwave", - "use-gpu" => "true", - "window-batch" => "2", - "recon-progress" => "false", - "random-seed" => opts["random-seed"], - ) -end - -function jitter_bandwidth_jobs(opts) - base = jitter_bandwidth_base_args(opts) - specs = [ - ("fjitter10_bw80", Dict("recon-bandwidth-khz" => "80", "frequency-jitter-percent" => "10"), "10% frequency jitter with very tight harmonic search bands."), - ("fjitter75_bw150", Dict("recon-bandwidth-khz" => "150", "frequency-jitter-percent" => "7.5"), "7.5% frequency jitter with 150 kHz harmonic search bands."), - ("fjitter9_bw180", Dict("recon-bandwidth-khz" => "180", "frequency-jitter-percent" => "9"), "9% frequency jitter with 180 kHz harmonic search bands."), - ("fjitter10_bw200", Dict("recon-bandwidth-khz" => "200", "frequency-jitter-percent" => "10"), "10% frequency jitter with moderate harmonic search bands."), - ("fjitter11_bw220", Dict("recon-bandwidth-khz" => "220", "frequency-jitter-percent" => "11"), "11% frequency jitter with 220 kHz harmonic search bands."), - ("fjitter10_bw400", Dict("recon-bandwidth-khz" => "400", "frequency-jitter-percent" => "10"), "10% frequency jitter with broad harmonic search bands."), - ("fjitter4_bw80", Dict("recon-bandwidth-khz" => "80", "frequency-jitter-percent" => "4"), "4% frequency jitter with 80 kHz bands (formula-matched)."), - ("fjitter20_bw80", Dict("recon-bandwidth-khz" => "80", "frequency-jitter-percent" => "20"), "20% frequency jitter with tight 80 kHz bands (under-matched)."), - ("fjitter30_bw80", Dict("recon-bandwidth-khz" => "80", "frequency-jitter-percent" => "30"), "30% frequency jitter with tight 80 kHz bands (heavily under-matched)."), - ("fjitter4_bw80", Dict("recon-bandwidth-khz" => "80", "frequency-jitter-percent" => "4"), "4% frequency jitter with 80 kHz bands (formula-matched, clean timing run)."), - ("fjitter2_bw60", Dict("recon-bandwidth-khz" => "60", "frequency-jitter-percent" => "2"), "2% frequency jitter with 60 kHz bands (under-matched, tight regime)."), - ("fjitter1_bw40", Dict("recon-bandwidth-khz" => "40", "frequency-jitter-percent" => "1"), "1% frequency jitter with 40 kHz bands (tight regime)."), - ("fjitter05_bw20", Dict("recon-bandwidth-khz" => "20", "frequency-jitter-percent" => "0.5"), "0.5% frequency jitter with 20 kHz bands (very tight regime)."), - ("fjitter05_bw40", Dict("recon-bandwidth-khz" => "40", "frequency-jitter-percent" => "0.5"), "0.5% frequency jitter with 40 kHz bands (under-matched)."), - ("fjitter15_bw40", Dict("recon-bandwidth-khz" => "40", "frequency-jitter-percent" => "1.5"), "1.5% frequency jitter with 40 kHz bands (over-matched)."), - ("fjitter1_bw20", Dict("recon-bandwidth-khz" => "20", "frequency-jitter-percent" => "1"), "1% frequency jitter with 20 kHz bands."), - ("fjitter1_bw40_hop40", Dict("recon-bandwidth-khz" => "40", "frequency-jitter-percent" => "1", "recon-hop-us" => "40"), "1% jitter, 40 kHz, hop=40us (no overlap)."), - ("fjitter1_bw40_hop10", Dict("recon-bandwidth-khz" => "40", "frequency-jitter-percent" => "1", "recon-hop-us" => "10"), "1% jitter, 40 kHz, hop=10us (2x overlap)."), - ("fjitter1_bw40_hop5", Dict("recon-bandwidth-khz" => "40", "frequency-jitter-percent" => "1", "recon-hop-us" => "5"), "1% jitter, 40 kHz, hop=5us (4x overlap)."), - ] - return [sim_job(id, merge_args(base, args); note=note) for (id, args, note) in specs] -end - -function jitter_bandwidth_main() - opts = jitter_bandwidth_opts(ARGS) - dry_run = parse_bool(opts["dry-run"]) - force = parse_bool(opts["force"]) - max_seconds = parse(Float64, opts["max-hours"]) * 3600 - timeout_seconds = parse(Float64, opts["per-run-timeout-min"]) * 60 - out_root = isempty(strip(opts["output-root"])) ? - joinpath(PROJECT_ROOT, "outputs", "$(timestamp())_pam_3d_jitter_bandwidth_sweep") : - abspath(opts["output-root"]) - mkpath(out_root) - - jobs = jitter_bandwidth_jobs(opts) - write_json(joinpath(out_root, "manifest.json"), Dict{String, Any}( - "created_at" => string(Dates.now()), - "output_root" => out_root, - "max_hours" => parse(Float64, opts["max-hours"]), - "per_run_timeout_min" => parse(Float64, opts["per-run-timeout-min"]), - "job_count" => length(jobs), - "selection_metric" => "source_f1", - "jobs" => jobs, - )) - - println("Output root: ", out_root) - println("Jobs queued: ", length(jobs)) - println("Max hours: ", opts["max-hours"], " | per-run timeout min: ", opts["per-run-timeout-min"]) - dry_run && println("Dry run only; no jobs will be executed.") - - rows = Dict{String, Any}[] - csv_path = joinpath(out_root, "results.csv") - start_all = time() - for (idx, job) in pairs(jobs) - remaining_seconds = max_seconds - (time() - start_all) - if !dry_run && remaining_seconds <= 0 - println("Time budget exhausted before job ", idx, "; stopping.") - break - end - job_timeout_seconds = dry_run ? timeout_seconds : min(timeout_seconds, remaining_seconds) - row = run_job(job, out_root, idx, length(jobs), job_timeout_seconds, dry_run, force) - push!(rows, row) - append_csv(csv_path, row) - write_json(joinpath(out_root, "results.json"), rows) - end - - completed = [row for row in rows if get(row, "status", "") in ("success", "skipped_existing") && haskey(row, "best_hasa_f1")] - if !isempty(completed) - best = first(completed) - for row in completed[2:end] - if get(row, "best_hasa_f1", -Inf) > get(best, "best_hasa_f1", -Inf) - best = row - end - end - println("\nBest HASA source F1: ", get(best, "best_hasa_f1", "n/a"), " (", best["id"], ")") - end - println("Results written to ", out_root) -end - -jitter_bandwidth_main() diff --git a/scripts/run_pam_3d_location_rescue_sweep.jl b/scripts/run_pam_3d_location_rescue_sweep.jl deleted file mode 100644 index 1c7c799..0000000 --- a/scripts/run_pam_3d_location_rescue_sweep.jl +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env julia - -include(joinpath(@__DIR__, "run_pam_3d_vessel_sweep.jl")) - -function rescue_sweep_jobs(opts) - base = vessel_base_args(opts) - weak_locations = [ - ("depth52", "52:0:0", "Deeper centered vessel."), - ("depth62", "62:0:0", "Deep centered vessel."), - ("y_p9", "42:9:0", "Moderate positive lateral-y offset."), - ("y_p18", "42:18:0", "Large positive lateral-y offset."), - ("diag_p12_m12", "42:12:-12", "Mixed-sign positive-y diagonal offset."), - ("diag_p12_p12", "42:12:12", "Positive-y/positive-z diagonal offset."), - ] - param_sets = [ - ("bw60_jitter2", Dict("recon-bandwidth-khz" => "60", "frequency-jitter-percent" => "2"), "60 kHz / 2% tight-band high-F1 setting."), - ("bw80_jitter4", Dict("recon-bandwidth-khz" => "80", "frequency-jitter-percent" => "4"), "80 kHz / 4% formula-matched setting."), - ] - jobs = Dict{String, Any}[] - for (loc_id, anchor, loc_note) in weak_locations - for (param_id, params, param_note) in param_sets - id = "rescue_$(loc_id)_$(param_id)" - args = merge_args(base, Dict("anchors-mm" => anchor), params) - push!(jobs, sim_job(id, args; note="$loc_note $param_note")) - end - end - return jobs -end - -function rescue_sweep_main() - opts = vessel_sweep_opts(ARGS) - dry_run = parse_bool(opts["dry-run"]) - force = parse_bool(opts["force"]) - max_seconds = parse(Float64, opts["max-hours"]) * 3600 - timeout_seconds = parse(Float64, opts["per-run-timeout-min"]) * 60 - out_root = isempty(strip(opts["output-root"])) ? - joinpath(PROJECT_ROOT, "outputs", "$(timestamp())_pam_3d_location_rescue_sweep") : - abspath(opts["output-root"]) - mkpath(out_root) - - jobs = rescue_sweep_jobs(opts) - write_json(joinpath(out_root, "manifest.json"), Dict{String, Any}( - "created_at" => string(Dates.now()), - "output_root" => out_root, - "max_hours" => parse(Float64, opts["max-hours"]), - "per_run_timeout_min" => parse(Float64, opts["per-run-timeout-min"]), - "job_count" => length(jobs), - "selection_metric" => "source_f1", - "jobs" => jobs, - )) - - println("Output root: ", out_root) - println("Jobs queued: ", length(jobs)) - println("Max hours: ", opts["max-hours"], " | per-run timeout min: ", opts["per-run-timeout-min"]) - dry_run && println("Dry run only; no jobs will be executed.") - - rows = Dict{String, Any}[] - csv_path = joinpath(out_root, "results.csv") - start_all = time() - for (idx, job) in pairs(jobs) - remaining_seconds = max_seconds - (time() - start_all) - if !dry_run && remaining_seconds <= 0 - println("Time budget exhausted before job ", idx, "; stopping.") - break - end - job_timeout_seconds = dry_run ? timeout_seconds : min(timeout_seconds, remaining_seconds) - row = run_job(job, out_root, idx, length(jobs), job_timeout_seconds, dry_run, force) - push!(rows, row) - append_csv(csv_path, row) - write_json(joinpath(out_root, "results.json"), rows) - write_json(joinpath(out_root, "leaderboard.json"), leaderboard_rows(rows)) - end - - leaders = leaderboard_rows(rows) - println() - println("3D location rescue sweep complete. Results: ", out_root) - if isempty(leaders) - println("No successful jobs with activity-boundary metrics yet.") - else - println("Top HASA source-F1:") - for row in first(leaders, min(10, length(leaders))) - @printf(" %.3f thr=%.2f prec=%.3f rec=%.3f %s\n", - Float64(row["best_hasa_f1"]), - Float64(row["best_hasa_threshold"]), - Float64(get(row, "best_hasa_precision", NaN)), - Float64(get(row, "best_hasa_recall", NaN)), - row["id"], - ) - end - end -end - -if abspath(PROGRAM_FILE) == abspath(@__FILE__) - rescue_sweep_main() -end diff --git a/scripts/run_pam_3d_vessel_sweep.jl b/scripts/run_pam_3d_vessel_sweep.jl deleted file mode 100644 index 24ef3f2..0000000 --- a/scripts/run_pam_3d_vessel_sweep.jl +++ /dev/null @@ -1,171 +0,0 @@ -#!/usr/bin/env julia - -include(joinpath(@__DIR__, "run_pam_overnight_sweep.jl")) - -function vessel_sweep_opts(args) - opts = parse_cli(args) - provided = Set(first(split(arg[3:end], "="; limit=2)) for arg in args if startswith(arg, "--")) - !("max-hours" in provided) && (opts["max-hours"] = "11") - !("per-run-timeout-min" in provided) && (opts["per-run-timeout-min"] = "25") - !("output-root" in provided) && (opts["output-root"] = "") - !("random-seed" in provided) && (opts["random-seed"] = "42") - !("auto-threshold-search" in provided) && (opts["auto-threshold-search"] = "true") - !("auto-threshold-min" in provided) && (opts["auto-threshold-min"] = "0.10") - !("auto-threshold-max" in provided) && (opts["auto-threshold-max"] = "0.95") - !("auto-threshold-step" in provided) && (opts["auto-threshold-step"] = "0.01") - return opts -end - -function vessel_base_args(opts) - return Dict( - "dimension" => "3", - "source-model" => "squiggle", - "gate-us" => "45", - "anchors-mm" => "42:0:0", - "vascular-length-mm" => "12", - "vascular-squiggle-amplitude-mm" => "0.3", - "vascular-squiggle-amplitude-x-mm" => "0.2", - "vascular-squiggle-wavelength-mm" => "8", - "squiggle-phase-x-deg" => "90", - "vascular-source-spacing-mm" => "0.5", - "vascular-min-separation-mm" => "0.25", - "vascular-radius-mm" => "1.0", - "harmonics" => "2,3,4", - "harmonic-amplitudes" => "1.0,0.6,0.3", - "aberrator" => "skull", - "skull-transducer-distance-mm" => "20", - "slice-index" => opts["slice-index"], - "axial-mm" => "70", - "transverse-mm" => "64", - "dx-mm" => "0.2", - "dy-mm" => "0.5", - "dz-mm" => "0.5", - "t-max-us" => "250", - "frequency-mhz" => "0.5", - "receiver-aperture-mm" => "full", - "source-phase-mode" => "random_phase_per_window", - "frequency-jitter-percent" => "1", - "recon-window-us" => "40", - "recon-hop-us" => "20", - "recon-bandwidth-khz" => "40", - "auto-threshold-search" => opts["auto-threshold-search"], - "auto-threshold-min" => opts["auto-threshold-min"], - "auto-threshold-max" => opts["auto-threshold-max"], - "auto-threshold-step" => opts["auto-threshold-step"], - "sim-mode" => "kwave", - "use-gpu" => "true", - "window-batch" => "2", - "recon-progress" => "false", - "random-seed" => opts["random-seed"], - ) -end - -function vessel_sweep_jobs(opts) - base = vessel_base_args(opts) - specs = [ - ("baseline_len12_mild_center", Dict(), "Current 40 kHz / 1% / hop 20 us baseline."), - - ("size_len6_center", Dict("vascular-length-mm" => "6"), "Short vessel segment at the baseline location."), - ("size_len18_center", Dict("vascular-length-mm" => "18"), "Longer vessel segment at the baseline location."), - ("size_dense_spacing03", Dict("vascular-source-spacing-mm" => "0.3", "vascular-min-separation-mm" => "0.15"), "Denser bubble sampling on the same centerline."), - ("size_sparse_spacing075", Dict("vascular-source-spacing-mm" => "0.75", "vascular-min-separation-mm" => "0.35"), "Sparser bubble sampling on the same centerline."), - ("metric_radius05", Dict("vascular-radius-mm" => "0.5"), "Tighter source/truth radius scoring sensitivity."), - ("metric_radius15", Dict("vascular-radius-mm" => "1.5"), "Looser source/truth radius scoring sensitivity."), - - ("shape_straight", Dict("vascular-squiggle-amplitude-mm" => "0", "vascular-squiggle-amplitude-x-mm" => "0"), "Straight centerline control."), - ("shape_wide_sine", Dict("vascular-squiggle-amplitude-mm" => "0.8", "vascular-squiggle-amplitude-x-mm" => "0.5"), "Higher-amplitude 3D squiggle."), - ("shape_tight_wave", Dict("vascular-squiggle-amplitude-mm" => "0.8", "vascular-squiggle-amplitude-x-mm" => "0.5", "vascular-squiggle-wavelength-mm" => "4"), "High-curvature short-wavelength squiggle."), - ("shape_long_wave", Dict("vascular-squiggle-amplitude-mm" => "0.8", "vascular-squiggle-amplitude-x-mm" => "0.5", "vascular-squiggle-wavelength-mm" => "12"), "Smoother long-wavelength squiggle."), - ("shape_axial_tilt", Dict("vascular-squiggle-slope" => "0.25"), "Centerline tilted in depth along its length."), - - ("loc_depth32_center", Dict("anchors-mm" => "32:0:0"), "Shallower centered vessel."), - ("loc_depth52_center", Dict("anchors-mm" => "52:0:0"), "Deeper centered vessel."), - ("loc_depth62_center", Dict("anchors-mm" => "62:0:0"), "Deep centered vessel near the lower part of the domain."), - ("loc_y_m18", Dict("anchors-mm" => "42:-18:0"), "Lateral-y offset toward one side of the aperture."), - ("loc_y_m9", Dict("anchors-mm" => "42:-9:0"), "Moderate negative lateral-y offset."), - ("loc_y_p9", Dict("anchors-mm" => "42:9:0"), "Moderate positive lateral-y offset."), - ("loc_y_p18", Dict("anchors-mm" => "42:18:0"), "Lateral-y offset toward the opposite side of the aperture."), - ("loc_z_m18", Dict("anchors-mm" => "42:0:-18"), "Lateral-z offset toward one side of the aperture."), - ("loc_z_m9", Dict("anchors-mm" => "42:0:-9"), "Moderate negative lateral-z offset."), - ("loc_z_p9", Dict("anchors-mm" => "42:0:9"), "Moderate positive lateral-z offset."), - ("loc_z_p18", Dict("anchors-mm" => "42:0:18"), "Lateral-z offset toward the opposite side of the aperture."), - ("loc_diag_m12_m12", Dict("anchors-mm" => "42:-12:-12"), "Diagonal negative y/z offset."), - ("loc_diag_m12_p12", Dict("anchors-mm" => "42:-12:12"), "Mixed-sign diagonal offset."), - ("loc_diag_p12_m12", Dict("anchors-mm" => "42:12:-12"), "Mixed-sign diagonal offset."), - ("loc_diag_p12_p12", Dict("anchors-mm" => "42:12:12"), "Diagonal positive y/z offset."), - ] - return [sim_job(id, merge_args(base, args); note=note) for (id, args, note) in specs] -end - -function vessel_sweep_main() - opts = vessel_sweep_opts(ARGS) - dry_run = parse_bool(opts["dry-run"]) - force = parse_bool(opts["force"]) - max_seconds = parse(Float64, opts["max-hours"]) * 3600 - timeout_seconds = parse(Float64, opts["per-run-timeout-min"]) * 60 - out_root = isempty(strip(opts["output-root"])) ? - joinpath(PROJECT_ROOT, "outputs", "$(timestamp())_pam_3d_vessel_sweep") : - abspath(opts["output-root"]) - mkpath(out_root) - - jobs = vessel_sweep_jobs(opts) - write_json(joinpath(out_root, "manifest.json"), Dict{String, Any}( - "created_at" => string(Dates.now()), - "output_root" => out_root, - "max_hours" => parse(Float64, opts["max-hours"]), - "per_run_timeout_min" => parse(Float64, opts["per-run-timeout-min"]), - "job_count" => length(jobs), - "fixed_reconstruction" => Dict( - "recon_bandwidth_khz" => 40, - "frequency_jitter_percent" => 1, - "recon_window_us" => 40, - "recon_hop_us" => 20, - ), - "selection_metric" => "source_f1", - "jobs" => jobs, - )) - - println("Output root: ", out_root) - println("Jobs queued: ", length(jobs)) - println("Max hours: ", opts["max-hours"], " | per-run timeout min: ", opts["per-run-timeout-min"]) - dry_run && println("Dry run only; no jobs will be executed.") - - rows = Dict{String, Any}[] - csv_path = joinpath(out_root, "results.csv") - start_all = time() - for (idx, job) in pairs(jobs) - remaining_seconds = max_seconds - (time() - start_all) - if !dry_run && remaining_seconds <= 0 - println("Time budget exhausted before job ", idx, "; stopping.") - break - end - job_timeout_seconds = dry_run ? timeout_seconds : min(timeout_seconds, remaining_seconds) - row = run_job(job, out_root, idx, length(jobs), job_timeout_seconds, dry_run, force) - push!(rows, row) - append_csv(csv_path, row) - write_json(joinpath(out_root, "results.json"), rows) - write_json(joinpath(out_root, "leaderboard.json"), leaderboard_rows(rows)) - end - - leaders = leaderboard_rows(rows) - println() - println("3D vessel sweep complete. Results: ", out_root) - if isempty(leaders) - println("No successful jobs with activity-boundary metrics yet.") - else - println("Top HASA source-F1:") - for row in first(leaders, min(10, length(leaders))) - @printf(" %.3f thr=%.2f prec=%.3f rec=%.3f %s\n", - Float64(row["best_hasa_f1"]), - Float64(row["best_hasa_threshold"]), - Float64(get(row, "best_hasa_precision", NaN)), - Float64(get(row, "best_hasa_recall", NaN)), - row["id"], - ) - end - end -end - -if abspath(PROGRAM_FILE) == abspath(@__FILE__) - vessel_sweep_main() -end diff --git a/scripts/run_pam_aperture_sweep.jl b/scripts/run_pam_aperture_sweep.jl deleted file mode 100644 index d3134cb..0000000 --- a/scripts/run_pam_aperture_sweep.jl +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env julia - -include(joinpath(@__DIR__, "run_pam_overnight_sweep.jl")) - -function aperture_sweep_opts(args) - opts = parse_cli(args) - get!(opts, "max-hours", "8.75") - get!(opts, "per-run-timeout-min", "90") - get!(opts, "output-root", "") - get!(opts, "anchors-mm", "45:0") - get!(opts, "simulation-random-seeds", "42,43,44") - get!(opts, "apertures-mm", "50,60,100,full,40,30,20") - get!(opts, "transverse-mm", "102.4") - get!(opts, "slice-index", "250") - get!(opts, "skull-transducer-distance-mm", "30") - get!(opts, "boundary-threshold-ratios", "0.6,0.65,0.7") - get!(opts, "random-seed", first(parse_string_list(opts["simulation-random-seeds"]))) - return opts -end - -function aperture_label(aperture::AbstractString) - lower = lowercase(strip(aperture)) - lower in ("full", "all", "none") && return "full" - return replace(lower, "." => "p") -end - -function aperture_sweep_jobs(opts) - apertures = parse_string_list(opts["apertures-mm"]) - seeds = parse_string_list(opts["simulation-random-seeds"]) - isempty(apertures) && error("--apertures-mm must contain at least one aperture.") - isempty(seeds) && error("--simulation-random-seeds must contain at least one seed.") - - base = Dict( - "source-model" => "squiggle", - "anchors-mm" => opts["anchors-mm"], - "vascular-length-mm" => "12", - "aberrator" => "skull", - "slice-index" => opts["slice-index"], - "skull-transducer-distance-mm" => opts["skull-transducer-distance-mm"], - "transverse-mm" => opts["transverse-mm"], - "receiver-aperture-mm" => "", - "boundary-threshold-ratios" => opts["boundary-threshold-ratios"], - "recon-min-window-energy-ratio" => "0.001", - "cavitation-model" => "harmonic-cos", - "harmonics" => "2,3,4", - "harmonic-amplitudes" => "1.0,0.6,0.3", - "source-phase-mode" => "random_phase_per_window", - "recon-window-us" => "20", - "recon-hop-us" => "10", - "recon-bandwidth-khz" => "500", - "t-max-us" => "500", - "frequency-jitter-percent" => "1", - ) - - jobs = Dict{String, Any}[] - for seed in seeds - for aperture in apertures - id = "aperture_ap$(aperture_label(aperture))_seed$(seed)" - args = merge_args( - base, - Dict( - "receiver-aperture-mm" => aperture, - "random-seed" => seed, - ), - ) - note = "Focused aperture sweep for h234 random-phase-per-window w20/bw500/t500 on a $(opts["transverse-mm"]) mm transverse grid." - push!(jobs, sim_job(id, args; note=note)) - end - end - return jobs -end - -function aperture_sweep_main() - opts = aperture_sweep_opts(ARGS) - dry_run = parse_bool(opts["dry-run"]) - force = parse_bool(opts["force"]) - max_seconds = parse(Float64, opts["max-hours"]) * 3600 - timeout_seconds = parse(Float64, opts["per-run-timeout-min"]) * 60 - out_root = isempty(strip(opts["output-root"])) ? - joinpath(PROJECT_ROOT, "outputs", "$(timestamp())_pam_aperture_sweep") : - abspath(opts["output-root"]) - mkpath(out_root) - - jobs = aperture_sweep_jobs(opts) - manifest = Dict{String, Any}( - "created_at" => string(Dates.now()), - "output_root" => out_root, - "max_hours" => parse(Float64, opts["max-hours"]), - "per_run_timeout_min" => parse(Float64, opts["per-run-timeout-min"]), - "apertures_mm" => parse_string_list(opts["apertures-mm"]), - "simulation_random_seeds" => parse_string_list(opts["simulation-random-seeds"]), - "transverse_mm" => opts["transverse-mm"], - "job_count" => length(jobs), - "jobs" => jobs, - ) - write_json(joinpath(out_root, "manifest.json"), manifest) - - println("Output root: ", out_root) - println("Jobs queued: ", length(jobs)) - println("Apertures mm: ", opts["apertures-mm"]) - println("Seeds: ", opts["simulation-random-seeds"]) - println("Transverse grid mm: ", opts["transverse-mm"]) - println("Max hours: ", opts["max-hours"], " | per-run timeout min: ", opts["per-run-timeout-min"]) - dry_run && println("Dry run only; no jobs will be executed.") - - rows = Dict{String, Any}[] - csv_path = joinpath(out_root, "results.csv") - start_all = time() - for (idx, job) in pairs(jobs) - elapsed_all = time() - start_all - remaining_seconds = max_seconds - elapsed_all - if !dry_run && remaining_seconds <= 0 - println("Time budget exhausted before job ", idx, "; stopping.") - break - end - job_timeout_seconds = dry_run ? timeout_seconds : min(timeout_seconds, remaining_seconds) - row = run_job(job, out_root, idx, length(jobs), job_timeout_seconds, dry_run, force) - push!(rows, row) - append_csv(csv_path, row) - write_json(joinpath(out_root, "results.json"), rows) - write_json(joinpath(out_root, "leaderboard.json"), leaderboard_rows(rows)) - end - - leaders = leaderboard_rows(rows) - println() - println("Aperture sweep complete. Results: ", out_root) - if isempty(leaders) - println("No successful jobs with activity-boundary metrics yet.") - else - println("Top HASA F1:") - for row in first(leaders, min(10, length(leaders))) - @printf(" %.3f thr=%.2f %s %s\n", - Float64(row["best_hasa_f1"]), - Float64(row["best_hasa_threshold"]), - row["id"], - row["out_dir"], - ) - end - end -end - -if abspath(PROGRAM_FILE) == abspath(@__FILE__) - aperture_sweep_main() -end diff --git a/scripts/run_pam_overnight_sweep.jl b/scripts/run_pam_overnight_sweep.jl deleted file mode 100644 index e27237e..0000000 --- a/scripts/run_pam_overnight_sweep.jl +++ /dev/null @@ -1,517 +0,0 @@ -#!/usr/bin/env julia - -using Dates -using JSON3 -using Printf - -const PROJECT_ROOT = normpath(joinpath(@__DIR__, "..")) -const PAM_SCRIPT = joinpath(PROJECT_ROOT, "scripts", "run_pam.jl") - -function parse_cli(args) - opts = Dict{String, String}( - "max-hours" => "8.75", - "per-run-timeout-min" => "90", - "output-root" => "", - "from-run-dir" => "", - "anchors-mm" => "45:0", - "random-seed" => "42", - "simulation-random-seeds" => "42,43,44,45,46", - "slice-index" => "250", - "skull-transducer-distance-mm" => "30", - "boundary-threshold-ratios" => "0.6,0.65,0.7", - "auto-threshold-search" => "true", - "auto-threshold-min" => "0.10", - "auto-threshold-max" => "0.95", - "auto-threshold-step" => "0.01", - "use-gpu" => "false", - "dry-run" => "false", - "force" => "false", - ) - for arg in args - startswith(arg, "--") || error("Unsupported argument format: $arg") - key_value = split(arg[3:end], "="; limit=2) - length(key_value) == 2 || error("Arguments must use --name=value, got: $arg") - opts[key_value[1]] = key_value[2] - end - return opts -end - -parse_bool(s::AbstractString) = lowercase(strip(s)) in ("1", "true", "yes", "on") -parse_string_list(s::AbstractString) = [strip(item) for item in split(s, ",") if !isempty(strip(item))] - -function timestamp() - return Dates.format(Dates.now(), "yyyymmdd_HHMMSS") -end - -function slug(s::AbstractString) - out = lowercase(strip(s)) - out = replace(out, r"[^a-z0-9]+" => "_") - out = replace(out, r"^_+|_+$" => "") - return isempty(out) ? "job" : out -end - -function merge_args(pairs...) - args = Dict{String, String}() - for group in pairs - for (key, value) in group - args[String(key)] = String(value) - end - end - return args -end - -function common_sim_args(opts; seed=opts["random-seed"]) - return Dict( - "source-model" => "squiggle", - "anchors-mm" => opts["anchors-mm"], - "vascular-length-mm" => "12", - "aberrator" => "skull", - "slice-index" => opts["slice-index"], - "skull-transducer-distance-mm" => opts["skull-transducer-distance-mm"], - "random-seed" => seed, - "use-gpu" => opts["use-gpu"], - "boundary-threshold-ratios" => opts["boundary-threshold-ratios"], - "auto-threshold-search" => opts["auto-threshold-search"], - "auto-threshold-min" => opts["auto-threshold-min"], - "auto-threshold-max" => opts["auto-threshold-max"], - "auto-threshold-step" => opts["auto-threshold-step"], - "recon-min-window-energy-ratio" => "0.001", - ) -end - -function recon_job(id, args; note="") - return Dict{String, Any}( - "id" => id, - "kind" => "cached_reconstruction", - "args" => args, - "note" => note, - ) -end - -function sim_job(id, args; note="") - return Dict{String, Any}( - "id" => id, - "kind" => "simulation", - "args" => args, - "note" => note, - ) -end - -function cached_reconstruction_jobs(opts) - from_run_dir = opts["from-run-dir"] - isempty(strip(from_run_dir)) && return Dict{String, Any}[] - base = Dict( - "from-run-dir" => from_run_dir, - "boundary-threshold-ratios" => opts["boundary-threshold-ratios"], - "auto-threshold-search" => opts["auto-threshold-search"], - "auto-threshold-min" => opts["auto-threshold-min"], - "auto-threshold-max" => opts["auto-threshold-max"], - "auto-threshold-step" => opts["auto-threshold-step"], - ) - specs = [ - ("cached_w20_bw300_step50", Dict("recon-window-us" => "20", "recon-hop-us" => "10", "recon-bandwidth-khz" => "300", "recon-step-um" => "50")), - ("cached_w20_bw500_step50", Dict("recon-window-us" => "20", "recon-hop-us" => "10", "recon-bandwidth-khz" => "500", "recon-step-um" => "50")), - ("cached_w20_bw700_step50", Dict("recon-window-us" => "20", "recon-hop-us" => "10", "recon-bandwidth-khz" => "700", "recon-step-um" => "50")), - ("cached_w30_bw300_step50", Dict("recon-window-us" => "30", "recon-hop-us" => "15", "recon-bandwidth-khz" => "300", "recon-step-um" => "50")), - ("cached_w30_bw500_step50", Dict("recon-window-us" => "30", "recon-hop-us" => "15", "recon-bandwidth-khz" => "500", "recon-step-um" => "50")), - ("cached_w40_bw500_step50", Dict("recon-window-us" => "40", "recon-hop-us" => "20", "recon-bandwidth-khz" => "500", "recon-step-um" => "50")), - ("cached_w20_bw500_step25", Dict("recon-window-us" => "20", "recon-hop-us" => "10", "recon-bandwidth-khz" => "500", "recon-step-um" => "25")), - ("cached_w30_bw500_step25", Dict("recon-window-us" => "30", "recon-hop-us" => "15", "recon-bandwidth-khz" => "500", "recon-step-um" => "25")), - ] - return [recon_job(id, merge_args(base, args); note="Reuse cached RF and sweep only reconstruction/window/bandwidth settings.") for (id, args) in specs] -end - -function simulation_jobs(opts) - seeds = parse_string_list(opts["simulation-random-seeds"]) - isempty(seeds) && (seeds = [opts["random-seed"]]) - primary_seed = first(seeds) - secondary_seeds = seeds[2:end] - - priority_specs = [ - ( - "sim_squiggle_h234_rpw_w20_bw500_t500", - Dict( - "cavitation-model" => "harmonic-cos", - "harmonics" => "2,3,4", - "harmonic-amplitudes" => "1.0,0.6,0.3", - "source-phase-mode" => "random_phase_per_window", - "recon-window-us" => "20", - "recon-hop-us" => "10", - "recon-bandwidth-khz" => "500", - "t-max-us" => "500", - "frequency-jitter-percent" => "1", - ), - "Current best harmonic squiggle random-phase-per-window setting.", - ), - ( - "sim_squiggle_h234_rpw_w30_bw500_t500", - Dict( - "cavitation-model" => "harmonic-cos", - "harmonics" => "2,3,4", - "harmonic-amplitudes" => "1.0,0.6,0.3", - "source-phase-mode" => "random_phase_per_window", - "recon-window-us" => "30", - "recon-hop-us" => "15", - "recon-bandwidth-khz" => "500", - "t-max-us" => "500", - "frequency-jitter-percent" => "1", - ), - "Longer window comparison.", - ), - ( - "sim_squiggle_gaussian_h234_rpw_w20_bw700_t500", - Dict( - "cavitation-model" => "gaussian-pulse", - "harmonics" => "2,3,4", - "harmonic-amplitudes" => "1.0,0.6,0.3", - "source-phase-mode" => "random_phase_per_window", - "recon-window-us" => "20", - "recon-hop-us" => "10", - "recon-bandwidth-khz" => "700", - "t-max-us" => "500", - "frequency-jitter-percent" => "1", - ), - "Gaussian pulse comparison using the same squiggle geometry.", - ), - ] - - jobs = Dict{String, Any}[] - - for (id, args, note) in priority_specs - push!(jobs, sim_job(id, merge_args(common_sim_args(opts; seed=primary_seed), args); note=note)) - end - - for seed in secondary_seeds - for (id, args, note) in priority_specs[1:min(7, length(priority_specs))] - seeded_id = "$(id)_seed$(seed)" - seeded_note = "$note Seed repeat to check robustness." - push!(jobs, sim_job(seeded_id, merge_args(common_sim_args(opts; seed=seed), args); note=seeded_note)) - end - end - - return jobs -end - -function make_command(job, out_dir) - args = copy(job["args"]) - args["out-dir"] = out_dir - cmd = `$(Base.julia_cmd()) --project=$(PROJECT_ROOT) $(PAM_SCRIPT)` - for key in sort(collect(keys(args))) - cmd = `$cmd --$key=$(args[key])` - end - return cmd -end - -function maybe_float_property(obj, field::Symbol) - hasproperty(obj, field) || return nothing - value = getproperty(obj, field) - isnothing(value) && return nothing - try - return Float64(value) - catch - return nothing - end -end - -function copy_float_property!(metrics, key::AbstractString, obj, field::Symbol) - value = maybe_float_property(obj, field) - isnothing(value) || (metrics[key] = value) - return metrics -end - -function read_summary_metrics(out_dir) - summary_path = joinpath(out_dir, "summary.json") - isfile(summary_path) || return Dict{String, Any}("summary_found" => false) - summary = try - JSON3.read(read(summary_path, String)) - catch err - return Dict{String, Any}( - "summary_found" => false, - "summary_error" => sprint(showerror, err), - ) - end - metrics = Dict{String, Any}("summary_found" => true) - if hasproperty(summary, :activity_boundary_metrics) - boundary_metrics = summary.activity_boundary_metrics - best_hasa = nothing - best_hasa_f1 = -Inf - best_geo = nothing - best_geo_f1 = -Inf - if hasproperty(boundary_metrics, :hasa) - for entry in boundary_metrics.hasa - f1 = maybe_float_property(entry, :f1) - if !isnothing(f1) && f1 > best_hasa_f1 - best_hasa = entry - best_hasa_f1 = f1 - end - end - end - if hasproperty(boundary_metrics, :geometric) - for entry in boundary_metrics.geometric - f1 = maybe_float_property(entry, :f1) - if !isnothing(f1) && f1 > best_geo_f1 - best_geo = entry - best_geo_f1 = f1 - end - end - end - if !isnothing(best_hasa) - metrics["best_hasa_f1"] = best_hasa_f1 - copy_float_property!(metrics, "best_hasa_threshold", best_hasa, :threshold_ratio) - copy_float_property!(metrics, "best_hasa_precision", best_hasa, :precision) - copy_float_property!(metrics, "best_hasa_recall", best_hasa, :recall) - end - if !isnothing(best_geo) - metrics["best_geo_f1"] = best_geo_f1 - copy_float_property!(metrics, "best_geo_threshold", best_geo, :threshold_ratio) - end - end - if hasproperty(summary, :hasa) - copy_float_property!(metrics, "hasa_psf_corr", summary.hasa, :psf_target_correlation) - copy_float_property!(metrics, "hasa_psf_l2", summary.hasa, :psf_target_normalized_l2_error) - copy_float_property!(metrics, "hasa_centroid_error_mm", summary.hasa, :centroid_error_mm) - end - return metrics -end - -function write_json(path, value) - open(path, "w") do io - JSON3.pretty(io, value) - println(io) - end -end - -function append_csv(path, row) - is_new = !isfile(path) - headers = [ - "id", - "kind", - "status", - "elapsed_min", - "best_hasa_f1", - "best_hasa_threshold", - "best_hasa_precision", - "best_hasa_recall", - "best_geo_f1", - "hasa_psf_corr", - "hasa_psf_l2", - "out_dir", - ] - open(path, "a") do io - if is_new - println(io, join(headers, ",")) - end - vals = [replace(string(get(row, h, "")), "," => ";") for h in headers] - println(io, join(vals, ",")) - end -end - -function terminate_process!(proc, io; grace_seconds::Real=15) - try - kill(proc) - catch err - println(io, "# SIGTERM error: ", err) - flush(io) - end - - deadline = time() + grace_seconds - while process_running(proc) && time() < deadline - sleep(1) - end - - if process_running(proc) - println(io, "# Process still running after SIGTERM; sending SIGKILL.") - flush(io) - try - kill(proc, Base.SIGKILL) - catch err - println(io, "# SIGKILL error: ", err) - flush(io) - end - deadline = time() + 5 - while process_running(proc) && time() < deadline - sleep(0.5) - end - end - return !process_running(proc) -end - -function wait_if_finished(proc, io) - process_running(proc) && return false - try - wait(proc) - return true - catch err - println(io) - println(io, "# wait(proc) error: ", err) - flush(io) - return false - end -end - -function process_success(proc) - try - return success(proc) - catch - return false - end -end - -function run_job(job, out_root, index, total, timeout_seconds, dry_run, force) - id = job["id"] - job_dir = joinpath(out_root, @sprintf("%02d_%s", index, slug(id))) - mkpath(job_dir) - out_dir = joinpath(job_dir, "run") - log_path = joinpath(job_dir, "run.log") - status_path = joinpath(job_dir, "status.json") - cmd = make_command(job, out_dir) - - if !force && isfile(joinpath(out_dir, "summary.json")) - metrics = read_summary_metrics(out_dir) - row = merge( - Dict{String, Any}( - "id" => id, - "kind" => job["kind"], - "status" => "skipped_existing", - "elapsed_min" => 0.0, - "out_dir" => out_dir, - "cmd" => string(cmd), - ), - metrics, - ) - write_json(status_path, row) - return row - end - - row = Dict{String, Any}( - "id" => id, - "kind" => job["kind"], - "status" => dry_run ? "dry_run" : "running", - "elapsed_min" => 0.0, - "out_dir" => out_dir, - "cmd" => string(cmd), - "note" => get(job, "note", ""), - ) - write_json(status_path, row) - - println() - println("[$index/$total] ", id) - println(" kind: ", job["kind"]) - println(" out: ", out_dir) - println(" log: ", log_path) - println(" cmd: ", cmd) - - if dry_run - return row - end - - start = time() - timed_out = false - exit_ok = false - open(log_path, "w") do io - println(io, "# ", Dates.now()) - println(io, "# ", cmd) - flush(io) - proc = run(pipeline(cmd; stdout=io, stderr=io); wait=false) - while process_running(proc) - sleep(5) - if timeout_seconds > 0 && (time() - start) > timeout_seconds - timed_out = true - println(io) - println(io, "# TIMEOUT after ", round((time() - start) / 60; digits=2), " min; killing process.") - flush(io) - terminate_process!(proc, io) - break - end - end - finished = wait_if_finished(proc, io) - exit_ok = !timed_out && finished && process_success(proc) - end - - elapsed_min = (time() - start) / 60 - metrics = read_summary_metrics(out_dir) - row = merge( - row, - metrics, - Dict{String, Any}( - "status" => timed_out ? "timeout" : (exit_ok ? "success" : "failed"), - "elapsed_min" => round(elapsed_min; digits=2), - ), - ) - write_json(status_path, row) - return row -end - -function leaderboard_rows(rows) - successful = [row for row in rows if get(row, "status", "") in ("success", "skipped_existing") && haskey(row, "best_hasa_f1")] - sort!(successful; by=row -> Float64(row["best_hasa_f1"]), rev=true) - return successful -end - -function main() - opts = parse_cli(ARGS) - dry_run = parse_bool(opts["dry-run"]) - force = parse_bool(opts["force"]) - max_seconds = parse(Float64, opts["max-hours"]) * 3600 - timeout_seconds = parse(Float64, opts["per-run-timeout-min"]) * 60 - out_root = isempty(strip(opts["output-root"])) ? - joinpath(PROJECT_ROOT, "outputs", "$(timestamp())_overnight_pam_sweep") : - abspath(opts["output-root"]) - mkpath(out_root) - - jobs = vcat(cached_reconstruction_jobs(opts), simulation_jobs(opts)) - manifest = Dict{String, Any}( - "created_at" => string(Dates.now()), - "output_root" => out_root, - "max_hours" => parse(Float64, opts["max-hours"]), - "per_run_timeout_min" => parse(Float64, opts["per-run-timeout-min"]), - "from_run_dir" => opts["from-run-dir"], - "job_count" => length(jobs), - "jobs" => jobs, - ) - write_json(joinpath(out_root, "manifest.json"), manifest) - - println("Output root: ", out_root) - println("Jobs queued: ", length(jobs)) - println("Max hours: ", opts["max-hours"], " | per-run timeout min: ", opts["per-run-timeout-min"]) - dry_run && println("Dry run only; no jobs will be executed.") - - rows = Dict{String, Any}[] - csv_path = joinpath(out_root, "results.csv") - start_all = time() - for (idx, job) in pairs(jobs) - elapsed_all = time() - start_all - remaining_seconds = max_seconds - elapsed_all - if !dry_run && remaining_seconds <= 0 - println("Time budget exhausted before job ", idx, "; stopping.") - break - end - job_timeout_seconds = dry_run ? timeout_seconds : min(timeout_seconds, remaining_seconds) - row = run_job(job, out_root, idx, length(jobs), job_timeout_seconds, dry_run, force) - push!(rows, row) - append_csv(csv_path, row) - write_json(joinpath(out_root, "results.json"), rows) - write_json(joinpath(out_root, "leaderboard.json"), leaderboard_rows(rows)) - end - - leaders = leaderboard_rows(rows) - println() - println("Sweep complete. Results: ", out_root) - if isempty(leaders) - println("No successful jobs with activity-boundary metrics yet.") - else - println("Top HASA F1:") - for row in first(leaders, min(5, length(leaders))) - @printf(" %.3f thr=%.2f %s %s\n", - Float64(row["best_hasa_f1"]), - Float64(row["best_hasa_threshold"]), - row["id"], - row["out_dir"], - ) - end - end -end - -if abspath(PROGRAM_FILE) == abspath(@__FILE__) - main() -end diff --git a/scripts/run_pam_sweep.jl b/scripts/run_pam_sweep.jl deleted file mode 100644 index 8cef29a..0000000 --- a/scripts/run_pam_sweep.jl +++ /dev/null @@ -1,521 +0,0 @@ -#!/usr/bin/env julia - -using Pkg -Pkg.activate(joinpath(@__DIR__, "..")) - -using Dates -using CairoMakie -using JLD2 -using JSON3 -using TranscranialFUS - -function parse_cli(args) - opts = Dict{String, String}( - "frequency-mhz" => "1.0", - "amplitude-pa" => "5e4", - "num-cycles" => "4", - "axial-mm" => "90", - "transverse-mm" => "60", - "dx-mm" => "0.2", - "dz-mm" => "0.2", - "receiver-aperture-mm" => "50", - "t-max-us" => "80", - "dt-ns" => "40", - "zero-pad-factor" => "4", - "peak-suppression-radius-mm" => "2.0", - "success-tolerance-mm" => "1.0", - "sweep-preset" => "paper", - "aberrator" => "skull", - "ct-path" => DEFAULT_CT_PATH, - "slice-index" => "250", - "skull-transducer-distance-mm" => "30", - "skull-cavity-margin-mm" => "1.0", - "bottom-margin-mm" => "10", - "hu-bone-thr" => "200", - "lens-depth-mm" => "12", - "lens-lateral-mm" => "0", - "lens-axial-radius-mm" => "3", - "lens-lateral-radius-mm" => "12", - "aberrator-c" => "1700", - "aberrator-rho" => "1150", - "use-gpu" => "false", - ) - - for arg in args - startswith(arg, "--") || error("Unsupported argument format: $arg") - parts = split(arg[3:end], "="; limit=2) - length(parts) == 2 || error("Arguments must use --name=value, got: $arg") - opts[parts[1]] = parts[2] - end - return opts -end - -slug_value(x; digits::Int=1) = replace(string(round(Float64(x); digits=digits)), "-" => "m", "." => "p") - -parse_bool(s::AbstractString) = lowercase(strip(s)) in ("1", "true", "yes", "on") - -function parse_float_list(spec::AbstractString) - isempty(strip(spec)) && return Float64[] - return [parse(Float64, strip(item)) for item in split(spec, ",") if !isempty(strip(item))] -end - -function parse_aberrator(s::AbstractString) - value = Symbol(lowercase(strip(s))) - value in (:none, :lens, :skull) || error("Unknown aberrator: $s") - return value -end - -function parse_receiver_aperture_mm(s::AbstractString) - value = lowercase(strip(s)) - value in ("none", "full", "all") && return nothing - return parse(Float64, value) * 1e-3 -end - -function parse_target_pairs_mm(spec::AbstractString) - isempty(strip(spec)) && return Tuple{Float64, Float64}[] - pairs_mm = Tuple{Float64, Float64}[] - for token in split(spec, ",") - stripped = strip(token) - isempty(stripped) && continue - parts = split(stripped, ":"; limit=2) - length(parts) == 2 || error("Expected target specification axial_mm:lateral_mm, got: $token") - push!(pairs_mm, (parse(Float64, strip(parts[1])), parse(Float64, strip(parts[2])))) - end - return pairs_mm -end - -function build_sweep_targets(axial_targets_mm, lateral_targets_mm, opts) - frequency_hz = parse(Float64, opts["frequency-mhz"]) * 1e6 - amplitude_pa = parse(Float64, opts["amplitude-pa"]) - num_cycles = parse(Int, opts["num-cycles"]) - - targets = PointSource2D[] - for axial_mm in axial_targets_mm, lateral_mm in lateral_targets_mm - push!(targets, PointSource2D( - depth=axial_mm * 1e-3, - lateral=lateral_mm * 1e-3, - frequency=frequency_hz, - amplitude=amplitude_pa, - num_cycles=num_cycles, - )) - end - return targets -end - -function default_output_dir(opts, sweep_preset, cfg) - timestamp = Dates.format(Dates.now(), "yyyymmdd_HHMMSS") - parts = String[ - timestamp, - "run_pam_sweep", - lowercase(opts["aberrator"]), - String(sweep_preset), - "f$(slug_value(parse(Float64, opts["frequency-mhz"]); digits=2))mhz", - "ap$(isnothing(cfg.receiver_aperture) ? "full" : slug_value(cfg.receiver_aperture * 1e3; digits=1) * "mm")", - "dx$(slug_value(cfg.dx * 1e3; digits=2))mm", - ] - if lowercase(opts["aberrator"]) == "skull" - insert!(parts, length(parts), "slice" * opts["slice-index"]) - insert!(parts, length(parts), "st$(slug_value(parse(Float64, opts["skull-transducer-distance-mm"]); digits=1))mm") - end - return joinpath(pwd(), "outputs", join(parts, "_")) -end - -function map_db(map::AbstractMatrix{<:Real}, ref::Real) - safe_ref = max(Float64(ref), eps(Float64)) - return 10 .* log10.(max.(Float64.(map), eps(Float64)) ./ safe_ref) -end - -function bubble_sizes(values::AbstractVector{<:Real}; min_size::Real=10, max_size::Real=34) - finite_values = Float64[v for v in values if isfinite(v)] - isempty(finite_values) && return fill(Float64(min_size), length(values)) - - lo = minimum(finite_values) - hi = maximum(finite_values) - if isapprox(lo, hi; atol=1e-9) - return fill(Float64((min_size + max_size) / 2), length(values)) - end - - scale = (Float64(max_size) - Float64(min_size)) / (hi - lo) - return [isfinite(v) ? Float64(min_size) + (Float64(v) - lo) * scale : Float64(min_size) for v in values] -end - -function matrix_points(axial_targets_mm, lateral_targets_mm, error_mm::AbstractMatrix) - xs = Float64[] - ys = Float64[] - vals = Float64[] - for (row, axial_mm) in pairs(axial_targets_mm), (col, lateral_mm) in pairs(lateral_targets_mm) - value = Float64(error_mm[row, col]) - isfinite(value) || continue - push!(xs, lateral_mm) - push!(ys, axial_mm) - push!(vals, value) - end - return xs, ys, vals -end - -function maybe_overlay_medium!(ax, c, depth_mm, lateral_mm, cfg) - if maximum(Float64.(c)) > cfg.c0 + 10 - level = cfg.c0 + 0.5 * (maximum(Float64.(c)) - cfg.c0) - contour!( - ax, - lateral_mm, - depth_mm, - Float64.(c)'; - levels=[level], - color=:white, - linewidth=1.5, - ) - end -end - -function scatter_truth!(ax, truth_mm) - scatter!(ax, [truth_mm[2]], [truth_mm[1]]; color=:red, marker=:x, markersize=14, strokewidth=2) -end - -function scatter_prediction!(ax, predicted_mm) - scatter!(ax, [predicted_mm[2]], [predicted_mm[1]]; color=:white, marker=:circle, markersize=12, strokecolor=:black, strokewidth=1.0) -end - -function case_filename(truth_mm) - return "case_z$(slug_value(truth_mm[1]; digits=1))mm_x$(slug_value(truth_mm[2]; digits=1))mm.png" -end - -function save_case_overview(path, c, cfg, case) - truth = case[:truth_mm] - geo_pred = case[:geo_predicted_mm] - hasa_pred = case[:hasa_predicted_mm] - kgrid = case[:kgrid] - depth_mm = depth_coordinates(kgrid, cfg) .* 1e3 - lateral_mm = kgrid.y_vec .* 1e3 - ref = max(maximum(Float64.(case[:pam_geo])), maximum(Float64.(case[:pam_hasa])), eps(Float64)) - - fig = Figure(size=(1000, 500)) - geo_title = "Uncorrected | err=$(round(case[:stats_geo][:mean_radial_error_mm]; digits=2)) mm" - hasa_title = "Corrected | err=$(round(case[:stats_hasa][:mean_radial_error_mm]; digits=2)) mm" - - ax_geo = Axis( - fig[1, 1]; - title=geo_title, - xlabel="Lateral [mm]", - ylabel="Depth [mm]", - aspect=DataAspect(), - yreversed=true, - ) - heatmap!(ax_geo, lateral_mm, depth_mm, map_db(case[:pam_geo], ref)'; colormap=:viridis, colorrange=(-30, 0)) - maybe_overlay_medium!(ax_geo, c, depth_mm, lateral_mm, cfg) - scatter_truth!(ax_geo, truth) - scatter_prediction!(ax_geo, geo_pred) - - ax_hasa = Axis( - fig[1, 2]; - title=hasa_title, - xlabel="Lateral [mm]", - ylabel="Depth [mm]", - aspect=DataAspect(), - yreversed=true, - ) - heatmap!(ax_hasa, lateral_mm, depth_mm, map_db(case[:pam_hasa], ref)'; colormap=:viridis, colorrange=(-30, 0)) - maybe_overlay_medium!(ax_hasa, c, depth_mm, lateral_mm, cfg) - scatter_truth!(ax_hasa, truth) - scatter_prediction!(ax_hasa, hasa_pred) - - Label( - fig[0, 1:2], - "z=$(round(truth[1]; digits=1)) mm, x=$(round(truth[2]; digits=1)) mm"; - fontsize=18, - tellwidth=false, - ) - - save(path, fig) -end - -function save_sweep_overview(path, c, cfg, sweep_results) - example_cases = sweep_results[:example_cases] - n_examples = length(example_cases) - n_examples > 0 || error("No example cases available for PAM sweep overview plotting.") - - ref = eps(Float64) - for case in example_cases - ref = max(ref, maximum(Float64.(case[:pam_geo])), maximum(Float64.(case[:pam_hasa]))) - end - - fig = Figure(size=(1800, 1100)) - - for (row, case) in pairs(example_cases) - truth = case[:truth_mm] - geo_pred = case[:geo_predicted_mm] - hasa_pred = case[:hasa_predicted_mm] - kgrid = case[:kgrid] - depth_mm = depth_coordinates(kgrid, cfg) .* 1e3 - lateral_mm = kgrid.y_vec .* 1e3 - - geo_title = "Uncorrected | z=$(round(truth[1]; digits=0)) mm, x=$(round(truth[2]; digits=0)) mm" - hasa_title = "Corrected | z=$(round(truth[1]; digits=0)) mm, x=$(round(truth[2]; digits=0)) mm" - - ax_geo = Axis( - fig[row, 1]; - title=geo_title, - xlabel=row == n_examples ? "Lateral [mm]" : "", - ylabel="Depth [mm]", - aspect=DataAspect(), - yreversed=true, - ) - heatmap!(ax_geo, lateral_mm, depth_mm, map_db(case[:pam_geo], ref)'; colormap=:viridis, colorrange=(-30, 0)) - maybe_overlay_medium!(ax_geo, c, depth_mm, lateral_mm, cfg) - scatter_truth!(ax_geo, truth) - scatter_prediction!(ax_geo, geo_pred) - - ax_hasa = Axis( - fig[row, 2]; - title=hasa_title, - xlabel=row == n_examples ? "Lateral [mm]" : "", - ylabel=row == 1 ? "" : "Depth [mm]", - aspect=DataAspect(), - yreversed=true, - ) - heatmap!(ax_hasa, lateral_mm, depth_mm, map_db(case[:pam_hasa], ref)'; colormap=:viridis, colorrange=(-30, 0)) - maybe_overlay_medium!(ax_hasa, c, depth_mm, lateral_mm, cfg) - scatter_truth!(ax_hasa, truth) - scatter_prediction!(ax_hasa, hasa_pred) - end - - xs_geo, ys_geo, vals_geo = matrix_points( - sweep_results[:axial_targets_mm], - sweep_results[:lateral_targets_mm], - sweep_results[:geo_error_mm], - ) - xs_hasa, ys_hasa, vals_hasa = matrix_points( - sweep_results[:axial_targets_mm], - sweep_results[:lateral_targets_mm], - sweep_results[:hasa_error_mm], - ) - finite_vals = vcat(vals_geo, vals_hasa) - color_max = isempty(finite_vals) ? 1.0 : max(maximum(finite_vals), eps(Float64)) - - ax_geo_err = Axis( - fig[1:n_examples, 3]; - title="Uncorrected", - xlabel="Lateral target [mm]", - ylabel="Axial target [mm]", - yreversed=true, - xticks=sweep_results[:lateral_targets_mm], - yticks=sweep_results[:axial_targets_mm], - ) - sc_geo = scatter!( - ax_geo_err, - xs_geo, - ys_geo; - color=vals_geo, - colormap=:thermal, - colorrange=(0, color_max), - markersize=bubble_sizes(vals_geo), - strokecolor=:black, - strokewidth=0.75, - ) - - ax_hasa_err = Axis( - fig[1:n_examples, 4]; - title="Corrected", - xlabel="Lateral target [mm]", - ylabel="Axial target [mm]", - yreversed=true, - xticks=sweep_results[:lateral_targets_mm], - yticks=sweep_results[:axial_targets_mm], - ) - sc_hasa = scatter!( - ax_hasa_err, - xs_hasa, - ys_hasa; - color=vals_hasa, - colormap=:thermal, - colorrange=(0, color_max), - markersize=bubble_sizes(vals_hasa), - strokecolor=:black, - strokewidth=0.75, - ) - Colorbar(fig[1:n_examples, 5], sc_hasa; label="Error [mm]") - - save(path, fig) -end - -function case_summary(case) - geo_pred = case[:geo_predicted_mm] - hasa_pred = case[:hasa_predicted_mm] - return Dict( - "truth_mm" => [case[:truth_mm][1], case[:truth_mm][2]], - "geometric" => Dict( - "predicted_mm" => [geo_pred[1], geo_pred[2]], - "stats" => case[:stats_geo], - ), - "hasa" => Dict( - "predicted_mm" => [hasa_pred[1], hasa_pred[2]], - "stats" => case[:stats_hasa], - ), - ) -end - -opts = parse_cli(ARGS) -aberrator = parse_aberrator(opts["aberrator"]) - -custom_axial_targets = haskey(opts, "axial-targets-mm") ? parse_float_list(opts["axial-targets-mm"]) : nothing -custom_lateral_targets = haskey(opts, "lateral-targets-mm") ? parse_float_list(opts["lateral-targets-mm"]) : nothing -sweep_preset, axial_targets_mm, lateral_targets_mm = TranscranialFUS._resolve_pam_sweep_targets( - opts["sweep-preset"]; - axial_targets_mm=custom_axial_targets, - lateral_targets_mm=custom_lateral_targets, -) - -targets = build_sweep_targets(axial_targets_mm, lateral_targets_mm, opts) -cfg = PAMConfig( - dx=parse(Float64, opts["dx-mm"]) * 1e-3, - dz=parse(Float64, opts["dz-mm"]) * 1e-3, - axial_dim=parse(Float64, opts["axial-mm"]) * 1e-3, - transverse_dim=parse(Float64, opts["transverse-mm"]) * 1e-3, - receiver_aperture=parse_receiver_aperture_mm(opts["receiver-aperture-mm"]), - t_max=parse(Float64, opts["t-max-us"]) * 1e-6, - dt=parse(Float64, opts["dt-ns"]) * 1e-9, - zero_pad_factor=parse(Int, opts["zero-pad-factor"]), - peak_suppression_radius=parse(Float64, opts["peak-suppression-radius-mm"]) * 1e-3, - success_tolerance=parse(Float64, opts["success-tolerance-mm"]) * 1e-3, -) -cfg = fit_pam_config( - cfg, - targets; - min_bottom_margin=parse(Float64, opts["bottom-margin-mm"]) * 1e-3, - reference_depth=aberrator == :skull ? parse(Float64, opts["skull-transducer-distance-mm"]) * 1e-3 : nothing, -) - -out_dir = if haskey(opts, "out-dir") && !isempty(strip(opts["out-dir"])) - opts["out-dir"] -else - default_output_dir(opts, sweep_preset, cfg) -end -mkpath(out_dir) - -example_targets_mm = haskey(opts, "example-targets-mm") ? parse_target_pairs_mm(opts["example-targets-mm"]) : nothing - -c, rho, medium_info = make_pam_medium( - cfg; - aberrator=aberrator, - lens_center_depth=parse(Float64, opts["lens-depth-mm"]) * 1e-3, - lens_center_lateral=parse(Float64, opts["lens-lateral-mm"]) * 1e-3, - lens_axial_radius=parse(Float64, opts["lens-axial-radius-mm"]) * 1e-3, - lens_lateral_radius=parse(Float64, opts["lens-lateral-radius-mm"]) * 1e-3, - c_aberrator=parse(Float64, opts["aberrator-c"]), - rho_aberrator=parse(Float64, opts["aberrator-rho"]), - ct_path=opts["ct-path"], - slice_index=parse(Int, opts["slice-index"]), - skull_to_transducer=parse(Float64, opts["skull-transducer-distance-mm"]) * 1e-3, - hu_bone_thr=parse(Int, opts["hu-bone-thr"]), -) - -requested_targets = targets -dropped_targets = Dict{Symbol, Any}[] -cavity_start_rows = nothing -if aberrator == :skull - targets, dropped_targets, cavity_start_rows = TranscranialFUS._filter_pam_targets_in_skull_cavity( - c, - cfg, - targets; - min_margin=parse(Float64, opts["skull-cavity-margin-mm"]) * 1e-3, - ) - isempty(targets) && error("No PAM sweep targets remain after enforcing skull-cavity placement.") -end - -case_dir = joinpath(out_dir, "cases") -mkpath(case_dir) -case_file_records = Dict{String, Any}[] - -function case_callback(case, results) - plot_case = copy(case) - plot_case[:pam_geo] = results[:pam_geo] - plot_case[:pam_hasa] = results[:pam_hasa] - plot_case[:kgrid] = results[:kgrid] - filename = case_filename(case[:truth_mm]) - relpath = joinpath("cases", filename) - save_case_overview(joinpath(out_dir, relpath), c, cfg, plot_case) - push!(case_file_records, Dict( - "truth_mm" => [case[:truth_mm][1], case[:truth_mm][2]], - "file" => relpath, - )) -end - -sweep_results = run_pam_sweep( - c, - rho, - targets, - cfg; - frequencies=[parse(Float64, opts["frequency-mhz"]) * 1e6], - example_targets_mm=example_targets_mm, - use_gpu=parse_bool(opts["use-gpu"]), - case_callback=case_callback, -) - -save_sweep_overview(joinpath(out_dir, "overview.png"), c, cfg, sweep_results) - -medium_summary = Dict{String, Any}() -for (key, value) in medium_info - key == :mask && continue - medium_summary[String(key)] = value -end - -summary = Dict( - "out_dir" => out_dir, - "sweep_preset" => String(sweep_preset), - "requested_axial_targets_mm" => axial_targets_mm, - "requested_lateral_targets_mm" => lateral_targets_mm, - "axial_targets_mm" => sweep_results[:axial_targets_mm], - "lateral_targets_mm" => sweep_results[:lateral_targets_mm], - "example_targets_mm" => [[target[1], target[2]] for target in sweep_results[:example_targets_mm]], - "requested_target_count" => length(requested_targets), - "retained_target_count" => length(targets), - "dropped_targets" => [ - Dict( - "truth_mm" => [drop[:truth_mm][1], drop[:truth_mm][2]], - "row" => drop[:row], - "col" => drop[:col], - "reason" => String(drop[:reason]), - "required_row" => get(drop, :required_row, nothing), - ) for drop in dropped_targets - ], - "per_case_files" => case_file_records, - "source_frequency_hz" => parse(Float64, opts["frequency-mhz"]) * 1e6, - "source_amplitude_pa" => parse(Float64, opts["amplitude-pa"]), - "source_num_cycles" => parse(Int, opts["num-cycles"]), - "config" => Dict( - "dx" => cfg.dx, - "dz" => cfg.dz, - "axial_dim" => cfg.axial_dim, - "transverse_dim" => cfg.transverse_dim, - "receiver_aperture" => cfg.receiver_aperture, - "t_max" => cfg.t_max, - "dt" => cfg.dt, - "c0" => cfg.c0, - "rho0" => cfg.rho0, - "PML_GUARD" => cfg.PML_GUARD, - "effective_pml_guard" => TranscranialFUS._pam_pml_guard(cfg), - "zero_pad_factor" => cfg.zero_pad_factor, - "peak_suppression_radius" => cfg.peak_suppression_radius, - "success_tolerance" => cfg.success_tolerance, - "skull_cavity_margin" => parse(Float64, opts["skull-cavity-margin-mm"]) * 1e-3, - "bottom_margin" => parse(Float64, opts["bottom-margin-mm"]) * 1e-3, - ), - "medium" => medium_summary, - "cases" => [case_summary(case) for case in sweep_results[:cases]], - "geometric_error_mm" => sweep_results[:geo_error_mm], - "hasa_error_mm" => sweep_results[:hasa_error_mm], - "geometric_peak_intensity" => sweep_results[:geo_peak_intensity], - "hasa_peak_intensity" => sweep_results[:hasa_peak_intensity], -) - -if !isnothing(cavity_start_rows) - summary["medium"]["cavity_start_rows"] = cavity_start_rows -end - -open(joinpath(out_dir, "summary.json"), "w") do io - JSON3.pretty(io, summary) -end - -@save joinpath(out_dir, "result.jld2") c rho cfg targets sweep_results medium_info sweep_preset axial_targets_mm lateral_targets_mm dropped_targets - -println("Saved PAM sweep outputs to $out_dir") diff --git a/scripts/smoke_test_3d.jl b/scripts/smoke_test_3d.jl deleted file mode 100644 index 6a82eb8..0000000 --- a/scripts/smoke_test_3d.jl +++ /dev/null @@ -1,72 +0,0 @@ -using Pkg -Pkg.activate(joinpath(@__DIR__, "..")) -using TranscranialFUS - -src = PointSource3D( - depth = 30e-3, - lateral_y = 2e-3, - lateral_z = -1e-3, - frequency = 0.5e6, - amplitude = 1.0, - phase = 0.0, - delay = 0.0, - num_cycles = 5.0, -) - -cfg = PAMConfig3D( - dx = 0.5e-3, - dy = 0.5e-3, - dz = 0.5e-3, - axial_dim = 60e-3, - transverse_dim_y = 32e-3, - transverse_dim_z = 32e-3, - dt = 80e-9, - t_max = 60e-6, - c0 = 1500.0, - rho0 = 1000.0, - tukey_ratio = 0.25, - zero_pad_factor = 2, -) - -println("Grid: $(pam_Nx(cfg))×$(pam_Ny(cfg))×$(pam_Nz(cfg)), Nt=$(pam_Nt(cfg))") - -c, rho, _ = make_pam_medium_3d(cfg) -grid = pam_grid_3d(cfg) -ny, nz, nt = pam_Ny(cfg), pam_Nz(cfg), pam_Nt(cfg) -rf = zeros(Float32, ny, nz, nt) -for iy in 1:ny, iz in 1:nz - dy_src = grid.y[iy] - src.lateral_y - dz_src = grid.z[iz] - src.lateral_z - r = sqrt(src.depth^2 + dy_src^2 + dz_src^2) - t0 = r / cfg.c0 - for it in 1:nt - te = (it - 1) * cfg.dt - t0 - src.delay - if te >= 0 && te <= src.num_cycles / src.frequency - rf[iy, iz, it] = Float32(sin(2π * src.frequency * te)) - end - end -end -println("RF: size=$(size(rf)), max=$(maximum(abs.(rf)))") - -rr = receiver_row(cfg) -expected_row = rr + round(Int, src.depth / cfg.dx) -mid_y = argmin(abs.(collect(grid.y) .- src.lateral_y)) -mid_z = argmin(abs.(collect(grid.z) .- src.lateral_z)) -println("Expected peak: row=$expected_row, col_y=$mid_y ($(round(grid.y[mid_y]*1e3;digits=1))mm), col_z=$mid_z ($(round(grid.z[mid_z]*1e3;digits=1))mm)") - -intensity, g, info = reconstruct_pam_3d( - rf, c, cfg; - corrected = true, - use_gpu = true, - show_progress = true, -) -println("Intensity: size=$(size(intensity)), max=$(maximum(intensity))") -println(" at expected peak: $(intensity[expected_row, mid_y, mid_z])") -slice_depth = intensity[expected_row, :, :] -println(" slice at expected depth: max=$(maximum(slice_depth)) at $(Tuple(argmax(slice_depth)))") - -stats = analyse_pam_3d(intensity, g, cfg, [src]) -println("Peak predicted: $(stats[:predicted_mm])") -println("Truth: $(stats[:truth_mm])") -println("Radial error: $(round(stats[:radial_errors_mm][1]; digits=2)) mm (tolerance=$(cfg.success_tolerance*1e3) mm)") -println("Success: $(stats[:num_success])/$(length([src]))") diff --git a/src/TranscranialFUS.jl b/src/TranscranialFUS.jl index 12a8cef..8fdf547 100644 --- a/src/TranscranialFUS.jl +++ b/src/TranscranialFUS.jl @@ -1,23 +1,27 @@ module TranscranialFUS +using Dates +using Printf using Statistics using Random using DICOM using FFTW using Interpolations -import CairoMakie +using CairoMakie import CondaPkg import CUDA if !haskey(ENV, "SSL_CERT_FILE") && isfile("/etc/ssl/cert.pem") ENV["SSL_CERT_FILE"] = "/etc/ssl/cert.pem" end import PythonCall +import JSON3 const DEFAULT_CT_PATH = normpath(joinpath(homedir(), "Desktop", "OBJ_0001")) const DEFAULT_ROI_INDEX_XYZ = (170, 190, 400) const DEFAULT_ROI_SIZE_XYZ = (705, 360, 450) export DEFAULT_CT_PATH, DEFAULT_ROI_INDEX_XYZ, DEFAULT_ROI_SIZE_XYZ +export source_summary export CTInfo, KGrid2D, SimulationConfig, SweepSettings, AnimationSettings, MediumType, Est export WATER, SKULL_IN_WATER, GEOMETRIC, HASA export parse_placement_mode, resolve_placement_mode @@ -26,15 +30,15 @@ export load_roi_resample_xy, load_default_ct export hu_to_rho_c, find_skull_boundaries, skull_mask_from_c_columnwise export make_medium_fixed_distance_from_skull, make_medium_fixed_transducer, make_medium export plot_hasa_results, focus, analyse_focus_2d, run_focus_case, kwave_available -export EmissionSource2D, PointSource2D, BubbleCluster2D, GaussianPulseCluster2D, PAMConfig, PAMWindowConfig, SourceVariabilityConfig, fit_pam_config, pam_Nx, pam_Ny, pam_Nt, pam_grid, receiver_row, receiver_col_range, depth_coordinates -export emission_frequencies, cavitation_model -export make_squiggle_bubble_sources, make_pam_medium, source_grid_index, simulate_point_sources, simulate_point_sources_3d, reconstruct_pam, reconstruct_pam_windowed, find_pam_peaks, find_pam_peaks_clean -export pam_truth_mask, pam_centerline_truth_mask, pam_source_map, pam_psf_blur, pam_psf_blurred_truth_map, threshold_pam_map, pam_intensity_metrics, analyse_pam_2d, analyse_pam_detection_2d, reconstruct_pam_case, run_pam_case, run_pam_sweep +export EmissionSource2D, PointSource2D, BubbleCluster2D, PAMConfig, PAMWindowConfig, SourceVariabilityConfig, fit_pam_config, pam_Nx, pam_Ny, pam_Nt, pam_grid, receiver_row, receiver_col_range, depth_coordinates +export emission_frequencies +export make_squiggle_bubble_sources, make_pam_medium, source_grid_index, simulate_point_sources, simulate_point_sources_3d, reconstruct_pam, reconstruct_pam_windowed, find_pam_peaks +export pam_truth_mask, pam_centerline_truth_mask, pam_source_map, pam_psf_blur, pam_psf_blurred_truth_map, threshold_pam_map, pam_intensity_metrics, analyse_pam_2d, analyse_pam_detection_2d, threshold_detection_stats, reconstruct_pam_case, run_pam_case export EmissionSource3D, PointSource3D, BubbleCluster3D, make_squiggle_bubble_sources_3d, make_network_bubble_sources_3d export PAMConfig3D, pam_Nz, pam_grid_3d, receiver_col_range_y, receiver_col_range_z, depth_coordinates_3d, fit_pam_config_3d, source_grid_index_3d export make_pam_medium_3d export PAMCUDASetup3D, reconstruct_pam_3d, reconstruct_pam_windowed_3d -export find_pam_peaks_3d, pam_truth_mask_3d, analyse_pam_3d +export find_pam_peaks_3d, pam_truth_mask_3d, source_detection_stats_3d, threshold_detection_stats_3d, best_threshold_entry_3d, threshold_outline_entries_3d, analyse_pam_3d, run_pam_case_3d include("focus.jl") include("pam.jl") diff --git a/src/common/kwave_helpers.jl b/src/common/kwave_helpers.jl new file mode 100644 index 0000000..7af7c41 --- /dev/null +++ b/src/common/kwave_helpers.jl @@ -0,0 +1,114 @@ +# Deterministic helpers for the Python k-Wave bridge. These are kept separate +# from live backend execution so default coverage can exercise them in CI. + +function _normalize_record(record::Union{Symbol, AbstractString}) + symbol = record isa Symbol ? record : Symbol(record) + symbol in (:p_rms, :p) || error("Unsupported record mode: $record") + return symbol +end + +function _as_sensor_matrix(array, expected_rows::Int, expected_cols::Int) + mat = Float64.(array) + ndims(mat) == 1 && return reshape(mat, :, 1) + if size(mat, 1) == expected_rows && size(mat, 2) == expected_cols + return mat + elseif size(mat, 1) == expected_cols && size(mat, 2) == expected_rows + return permutedims(mat) + end + error("Unexpected sensor data shape $(size(mat)); expected ($expected_rows, $expected_cols) or ($expected_cols, $expected_rows).") +end + +function _validate_point_source_inputs( + c::AbstractMatrix{<:Real}, + rho::AbstractMatrix{<:Real}, + sources::AbstractVector{<:EmissionSource2D}, + cfg::PAMConfig, +) + isempty(sources) && error("At least one emission source is required.") + nx, ny = size(c) + size(rho) == size(c) || error("Density map must have the same size as the sound-speed map.") + nx == pam_Nx(cfg) || error("Sound-speed map height $nx does not match PAMConfig height $(pam_Nx(cfg)).") + ny == pam_Ny(cfg) || error("Sound-speed map width $ny does not match PAMConfig width $(pam_Ny(cfg)).") + return nx, ny +end + +function _validate_point_source_inputs_3d( + c::AbstractArray{<:Real, 3}, + rho::AbstractArray{<:Real, 3}, + sources::AbstractVector{<:EmissionSource3D}, + cfg::PAMConfig3D, +) + isempty(sources) && error("At least one emission source is required.") + nx, ny, nz = pam_Nx(cfg), pam_Ny(cfg), pam_Nz(cfg) + size(c) == (nx, ny, nz) || error("Sound-speed map size $(size(c)) does not match PAMConfig3D ($nx, $ny, $nz).") + size(rho) == (nx, ny, nz) || error("Density map size $(size(rho)) does not match PAMConfig3D ($nx, $ny, $nz).") + return nx, ny, nz +end + +function _indexed_sources_2d(sources::AbstractVector{<:EmissionSource2D}, cfg::PAMConfig, kgrid::KGrid2D, nx::Int) + indexed_sources = [(source_grid_index(src, cfg, kgrid), src) for src in sources] + sort!(indexed_sources; by=entry -> first(entry)[1] + (first(entry)[2] - 1) * nx) + return indexed_sources +end + +function _indexed_sources_3d(sources::AbstractVector{<:EmissionSource3D}, cfg::PAMConfig3D, nx::Int, ny::Int) + indexed_sources = [(source_grid_index_3d(src, cfg), src) for src in sources] + sort!(indexed_sources; by=entry -> begin + row, cy, cz = first(entry) + row + (cy - 1) * nx + (cz - 1) * nx * ny + end) + return indexed_sources +end + +function _group_sources_2d(indexed_sources) + grouped_sources = Vector{Tuple{Tuple{Int, Int}, Vector{EmissionSource2D}}}() + for (grid_index, src) in indexed_sources + if !isempty(grouped_sources) && first(last(grouped_sources)) == grid_index + push!(last(grouped_sources)[2], src) + else + push!(grouped_sources, (grid_index, EmissionSource2D[src])) + end + end + return grouped_sources +end + +function _group_sources_3d(indexed_sources) + grouped_sources = Vector{Tuple{Tuple{Int, Int, Int}, Vector{EmissionSource3D}}}() + for (grid_index, src) in indexed_sources + if !isempty(grouped_sources) && first(last(grouped_sources)) == grid_index + push!(last(grouped_sources)[2], src) + else + push!(grouped_sources, (grid_index, EmissionSource3D[src])) + end + end + return grouped_sources +end + +function _unique_emission_frequencies(indexed_sources) + all_freqs = Float64[] + for (_, src) in indexed_sources + append!(all_freqs, _emission_frequencies(src)) + end + return unique(all_freqs) +end + +function _kwave_info_2d(row::Int, col_range::UnitRange{Int}, source_indices, sources, grouped_sources) + return Dict{Symbol, Any}( + :receiver_row => row, + :receiver_cols => col_range, + :source_indices => source_indices, + :num_input_sources => length(sources), + :num_source_points => length(grouped_sources), + ) +end + +function _kwave_info_3d(row::Int, col_range_y::UnitRange{Int}, col_range_z::UnitRange{Int}, source_indices, sources, grouped_sources) + return Dict{Symbol, Any}( + :receiver_row => row, + :receiver_cols_y => col_range_y, + :receiver_cols_z => col_range_z, + :source_indices => source_indices, + :num_input_sources => length(sources), + :num_source_points => length(grouped_sources), + ) +end diff --git a/src/common/kwave_wrapper.jl b/src/common/kwave_wrapper.jl index 484f412..894173d 100644 --- a/src/common/kwave_wrapper.jl +++ b/src/common/kwave_wrapper.jl @@ -30,12 +30,6 @@ function kwave_available() end end -function _normalize_record(record::Union{Symbol, AbstractString}) - symbol = record isa Symbol ? record : Symbol(record) - symbol in (:p_rms, :p) || error("Unsupported record mode: $record") - return symbol -end - function _py_bool_matrix(np, rows::Int, cols::Int) return np.zeros((rows, cols), dtype=PythonCall.pybuiltins.bool) end @@ -44,17 +38,6 @@ function _py_float_matrix(np, rows::Int, cols::Int) return np.zeros((rows, cols), dtype=np.float64) end -function _as_sensor_matrix(array, expected_rows::Int, expected_cols::Int) - mat = Float64.(array) - ndims(mat) == 1 && return reshape(mat, :, 1) - if size(mat, 1) == expected_rows && size(mat, 2) == expected_cols - return mat - elseif size(mat, 1) == expected_cols && size(mat, 2) == expected_rows - return permutedims(mat) - end - error("Unexpected sensor data shape $(size(mat)); expected ($expected_rows, $expected_cols) or ($expected_cols, $expected_rows).") -end - function _simulate_kwave( c::AbstractMatrix{<:Real}, rho::AbstractMatrix{<:Real}, @@ -135,7 +118,7 @@ function _simulate_kwave( ) exec_opts = mods.execopts.SimulationExecutionOptions( is_gpu_simulation=use_gpu, - delete_data=false, + delete_data=true, ) data = mods.kspace.kspaceFirstOrder2D( @@ -162,6 +145,7 @@ function simulate_point_sources( sources::AbstractVector{<:EmissionSource2D}, cfg::PAMConfig; use_gpu::Bool=false, + kwave_data_path::Union{Nothing, AbstractString}=nothing, ) isempty(sources) && error("At least one emission source is required.") @@ -230,30 +214,44 @@ function simulate_point_sources( sensor.mask = sensor_mask sensor.record = pc.pybuiltins.list(("p",)) + sim_dir = mktempdir(isnothing(kwave_data_path) ? tempdir() : String(kwave_data_path)) sim_opts = mods.simopts.SimulationOptions( pml_inside=false, pml_size=pml_guard, data_recast=false, save_to_disk=true, + data_path=sim_dir, ) exec_opts = mods.execopts.SimulationExecutionOptions( is_gpu_simulation=use_gpu, - delete_data=false, + delete_data=true, ) - data = mods.kspace.kspaceFirstOrder2D( - kgrid=deepcopy_py(py_kgrid), - medium=deepcopy_py(medium), - source=deepcopy_py(source), - sensor=deepcopy_py(sensor), - simulation_options=sim_opts, - execution_options=exec_opts, - ) - - sensor_data_py = np.array(data["p"], dtype=np.float64) - sensor_data = _as_sensor_matrix(pc.pyconvert(Array, sensor_data_py), length(col_range), nt) + sensor_data = let + data = mods.kspace.kspaceFirstOrder2D( + kgrid=deepcopy_py(py_kgrid), + medium=deepcopy_py(medium), + source=deepcopy_py(source), + sensor=deepcopy_py(sensor), + simulation_options=sim_opts, + execution_options=exec_opts, + ) + raw = pc.pyconvert(Array, np.array(data["p"], dtype=np.float64)) + _as_sensor_matrix(raw, length(col_range), nt) + # data and intermediate Python objects go out of scope here + end rf = zeros(Float64, ny, nt) rf[col_range, :] .= sensor_data + # Retry rm: Python's refcounting closes h5py handles as the let-scoped + # Py objects are finalized by Julia's natural GC between retries. + for attempt in 1:10 + try + rm(sim_dir; recursive=true, force=true) + break + catch + attempt < 10 && sleep(0.5) + end + end info = Dict{Symbol, Any}( :receiver_row => row, @@ -271,6 +269,7 @@ function simulate_point_sources_3d( sources::AbstractVector{<:EmissionSource3D}, cfg::PAMConfig3D; use_gpu::Bool=false, + kwave_data_path::Union{Nothing, AbstractString}=nothing, ) isempty(sources) && error("At least one emission source is required.") @@ -345,29 +344,34 @@ function simulate_point_sources_3d( sensor.mask = sensor_mask sensor.record = pc.pybuiltins.list(("p",)) + sim_dir = mktempdir(isnothing(kwave_data_path) ? tempdir() : String(kwave_data_path)) sim_opts = mods.simopts.SimulationOptions( pml_inside=false, pml_size=pml_guard, data_recast=false, save_to_disk=true, + data_path=sim_dir, ) exec_opts = mods.execopts.SimulationExecutionOptions( is_gpu_simulation=use_gpu, - delete_data=false, + delete_data=true, ) - data = mods.kspace3d.kspaceFirstOrder3D( - kgrid=deepcopy_py(py_kgrid), - medium=deepcopy_py(medium), - source=deepcopy_py(source), - sensor=deepcopy_py(sensor), - simulation_options=sim_opts, - execution_options=exec_opts, - ) - - sensor_data_py = np.array(data["p"], dtype=np.float64) - # k-Wave returns (Nt, ny*nz) in Python (row-major). pyconvert gives (ny*nz, Nt) in Julia. - sensor_data_flat = pc.pyconvert(Matrix{Float64}, sensor_data_py) # (ny*nz, Nt) or (Nt, ny*nz) + # Wrap in let so data/sensor_data_py go out of scope before GC, releasing h5py handles. + sensor_data_flat = let + data = mods.kspace3d.kspaceFirstOrder3D( + kgrid=deepcopy_py(py_kgrid), + medium=deepcopy_py(medium), + source=deepcopy_py(source), + sensor=deepcopy_py(sensor), + simulation_options=sim_opts, + execution_options=exec_opts, + ) + # k-Wave returns (Nt, ny*nz) in Python (row-major). pyconvert gives (ny*nz, Nt) in Julia. + flat = pc.pyconvert(Matrix{Float64}, np.array(data["p"], dtype=np.float64)) + # data and intermediate Python objects go out of scope here + flat + end # Normalise to (ny*nz, Nt) regardless of which axis k-Wave put time on. if size(sensor_data_flat, 1) == nt && size(sensor_data_flat, 2) == ny * nz sensor_data_flat = permutedims(sensor_data_flat) # → (ny*nz, Nt) @@ -376,6 +380,16 @@ function simulate_point_sources_3d( end # Julia reshape is column-major: ny varies fastest, matching k-Wave's Fortran-order enumeration. rf = reshape(sensor_data_flat, ny, nz, nt) + # Retry rm: Python's refcounting closes h5py handles as the let-scoped + # Py objects are finalized by Julia's natural GC between retries. + for attempt in 1:10 + try + rm(sim_dir; recursive=true, force=true) + break + catch + attempt < 10 && sleep(0.5) + end + end info = Dict{Symbol, Any}( :receiver_row => row, diff --git a/src/pam.jl b/src/pam.jl index c145087..4d50c5c 100644 --- a/src/pam.jl +++ b/src/pam.jl @@ -1,14 +1,22 @@ # PAM is split by responsibility under src/pam/. Keep this file as the public include point. -include("pam/sources.jl") -include("pam/config.jl") -include("pam/medium.jl") -include("pam/reconstruction.jl") -include("pam/analysis.jl") -include("pam/workflow.jl") -include("pam/sweep.jl") -include("pam/sources3d.jl") -include("pam/config3d.jl") -include("pam/medium3d.jl") -include("pam/reconstruction3d.jl") -include("pam/analysis3d.jl") +include("pam/2d/sources.jl") +include("pam/2d/config.jl") +include("pam/2d/medium.jl") +include("pam/2d/reconstruction.jl") +include("pam/2d/analysis.jl") +include("pam/2d/workflow.jl") +include("pam/3d/sources3d.jl") +include("pam/3d/config3d.jl") +include("pam/3d/medium3d.jl") +include("pam/3d/reconstruction3d.jl") +include("pam/3d/analysis3d.jl") +include("pam/setup/config.jl") +include("pam/setup/sources.jl") +include("pam/setup/medium.jl") +include("pam/setup/summary.jl") +include("pam/setup/runner.jl") +include("pam/3d/workflow3d.jl") +include("pam/2d/plots.jl") +include("pam/3d/plots3d.jl") +include("common/kwave_helpers.jl") include("common/kwave_wrapper.jl") diff --git a/src/pam/analysis.jl b/src/pam/2d/analysis.jl similarity index 86% rename from src/pam/analysis.jl rename to src/pam/2d/analysis.jl index b64b9a2..54a299a 100644 --- a/src/pam/analysis.jl +++ b/src/pam/2d/analysis.jl @@ -119,7 +119,6 @@ function _default_psf_widths( freqs = isnothing(frequencies) || isempty(frequencies) ? nothing : Float64.(frequencies) if isnothing(freqs) - # Fallback: assume one wavelength worth of structure lambda = cfg.c0 / 5e5 lateral = lambda * depth / aperture axial = 2 * lambda * (depth / aperture)^2 @@ -140,93 +139,6 @@ function _default_psf_widths( return max(axial, 2 * cfg.dx), max(lateral, 2 * cfg.dz) end -""" - find_pam_peaks_clean(intensity, kgrid, cfg; n_peaks, frequencies=nothing, - psf_axial_fwhm=nothing, psf_lateral_fwhm=nothing, - loop_gain=0.1, max_iter=500, threshold_ratio=1e-2, - suppression_radius=nothing) - -Iterative CLEAN (Högbom) peak detector for PAM intensity maps. Each iteration -finds the brightest residual pixel, adds `loop_gain * peak` to the accumulator, -and subtracts a scaled Gaussian PSF from the residual. The `n_peaks` brightest -maxima in the accumulator are returned. If `suppression_radius` is not given, -it defaults to the lateral PSF FWHM, which lets sources as close as one PSF -width apart be resolved distinctly. -""" -function find_pam_peaks_clean( - intensity::AbstractMatrix{<:Real}, - kgrid::KGrid2D, - cfg::PAMConfig; - n_peaks::Integer, - frequencies::Union{Nothing, AbstractVector{<:Real}}=nothing, - psf_axial_fwhm::Union{Nothing, Real}=nothing, - psf_lateral_fwhm::Union{Nothing, Real}=nothing, - loop_gain::Real=0.1, - max_iter::Integer=500, - threshold_ratio::Real=1e-2, - suppression_radius::Union{Nothing, Real}=nothing, -) - 0 < loop_gain <= 1 || error("loop_gain must lie in (0, 1].") - n_peaks > 0 || error("n_peaks must be positive.") - - residual = copy(Float64.(intensity)) - row_start = receiver_row(cfg) + 1 - row_stop = size(residual, 1) - row_start <= row_stop || error("No valid reconstruction rows remain.") - residual[1:(row_start - 1), :] .= -Inf - if row_stop < size(residual, 1) - residual[(row_stop + 1):end, :] .= -Inf - end - - ax_fwhm, lat_fwhm = if isnothing(psf_axial_fwhm) || isnothing(psf_lateral_fwhm) - _default_psf_widths(cfg, kgrid, frequencies) - else - Float64(psf_axial_fwhm), Float64(psf_lateral_fwhm) - end - ax_fwhm = something(psf_axial_fwhm, ax_fwhm) - lat_fwhm = something(psf_lateral_fwhm, lat_fwhm) - - σ_ax_cells = max(1.0, Float64(ax_fwhm) / (cfg.dx * 2.3548)) - σ_lat_cells = max(1.0, Float64(lat_fwhm) / (cfg.dz * 2.3548)) - half_ax = max(1, ceil(Int, 3 * σ_ax_cells)) - half_lat = max(1, ceil(Int, 3 * σ_lat_cells)) - - finite_mask = isfinite.(residual) - any(finite_mask) || return Tuple{Int, Int}[] - peak_init = maximum(residual[finite_mask]) - peak_init > 0 || return Tuple{Int, Int}[] - threshold = peak_init * Float64(threshold_ratio) - - accum = zeros(Float64, size(residual)) - nx, ny = size(residual) - - for _ in 1:Int(max_iter) - idx = Tuple(argmax(residual)) - pv = residual[idx...] - (!isfinite(pv) || pv < threshold) && break - - scale = Float64(loop_gain) * pv - r0, c0 = idx - accum[r0, c0] += scale - - r1 = max(1, r0 - half_ax) - r2 = min(nx, r0 + half_ax) - c1 = max(1, c0 - half_lat) - c2 = min(ny, c0 + half_lat) - @inbounds for r in r1:r2 - dr = (r - r0) / σ_ax_cells - for c in c1:c2 - dc = (c - c0) / σ_lat_cells - weight = exp(-0.5 * (dr^2 + dc^2)) - residual[r, c] -= scale * weight - end - end - end - - sup_radius = isnothing(suppression_radius) ? Float64(lat_fwhm) : Float64(suppression_radius) - return find_pam_peaks(accum, kgrid, cfg; n_peaks=n_peaks, suppression_radius=sup_radius) -end - function pam_truth_mask( sources::AbstractVector{<:EmissionSource2D}, kgrid::KGrid2D, @@ -324,8 +236,7 @@ function pam_centerline_truth_mask( end _source_activity_weight(src::PointSource2D) = abs(src.amplitude) -_source_activity_weight(src::BubbleCluster2D) = abs(src.amplitude * src.n_bubbles) -_source_activity_weight(src::GaussianPulseCluster2D) = abs(src.amplitude * src.n_bubbles) +_source_activity_weight(src::BubbleCluster2D) = abs(src.amplitude) """ pam_source_map(sources, kgrid, cfg; weights=:amplitude) @@ -795,6 +706,25 @@ function analyse_pam_detection_2d( ) end +function threshold_detection_stats(intensity, kgrid, cfg, sources; threshold_ratios, truth_radius, truth_mask, frequencies) + return [ + merge( + Dict(:threshold_ratio => ratio), + analyse_pam_detection_2d( + intensity, + kgrid, + cfg, + sources; + truth_radius=truth_radius, + threshold_ratio=ratio, + truth_mask=truth_mask, + frequencies=frequencies, + ), + ) + for ratio in threshold_ratios + ] +end + function analyse_pam_2d( intensity::AbstractMatrix{<:Real}, kgrid::KGrid2D, @@ -803,34 +733,11 @@ function analyse_pam_2d( n_peaks::Union{Nothing, Integer}=nothing, success_tolerance::Real=cfg.success_tolerance, suppression_radius::Real=cfg.peak_suppression_radius, - peak_method::Symbol=:argmax, - frequencies::Union{Nothing, AbstractVector{<:Real}}=nothing, - clean_loop_gain::Real=0.1, - clean_max_iter::Integer=500, - clean_threshold_ratio::Real=1e-2, - clean_psf_axial_fwhm::Union{Nothing, Real}=nothing, - clean_psf_lateral_fwhm::Union{Nothing, Real}=nothing, ) n_truth = length(sources) n_truth > 0 || error("At least one emission source is required for PAM analysis.") n_find = isnothing(n_peaks) ? n_truth : Int(n_peaks) - peaks = if peak_method == :clean - find_pam_peaks_clean( - intensity, kgrid, cfg; - n_peaks=n_find, - frequencies=frequencies, - psf_axial_fwhm=clean_psf_axial_fwhm, - psf_lateral_fwhm=clean_psf_lateral_fwhm, - loop_gain=clean_loop_gain, - max_iter=clean_max_iter, - threshold_ratio=clean_threshold_ratio, - suppression_radius=nothing, - ) - elseif peak_method == :argmax - find_pam_peaks(intensity, kgrid, cfg; n_peaks=n_find, suppression_radius=suppression_radius) - else - error("Unknown peak_method: $peak_method (expected :argmax or :clean).") - end + peaks = find_pam_peaks(intensity, kgrid, cfg; n_peaks=n_find, suppression_radius=suppression_radius) length(peaks) == n_truth || error("Expected to recover $n_truth peaks, found $(length(peaks)).") depth = depth_coordinates(kgrid, cfg) diff --git a/src/pam/config.jl b/src/pam/2d/config.jl similarity index 100% rename from src/pam/config.jl rename to src/pam/2d/config.jl diff --git a/src/pam/medium.jl b/src/pam/2d/medium.jl similarity index 80% rename from src/pam/medium.jl rename to src/pam/2d/medium.jl index 8b9abdc..1b6888e 100644 --- a/src/pam/medium.jl +++ b/src/pam/2d/medium.jl @@ -37,12 +37,6 @@ end function make_pam_medium( cfg::PAMConfig; aberrator::Symbol=:none, - lens_center_depth::Real=20e-3, - lens_center_lateral::Real=0.0, - lens_axial_radius::Real=4e-3, - lens_lateral_radius::Real=12e-3, - c_aberrator::Real=1700.0, - rho_aberrator::Real=1150.0, hu_vol::Union{Nothing, AbstractArray{<:Real, 3}}=nothing, spacing_m::Union{Nothing, NTuple{3, <:Real}}=nothing, ct_path::AbstractString=DEFAULT_CT_PATH, @@ -133,32 +127,8 @@ function make_pam_medium( :hu_bone_thr => Int(hu_bone_thr), :ct_path => ct_path, ) - elseif aberrator != :lens - error("Unknown PAM medium aberrator: $aberrator") end - depth = depth_coordinates(kgrid, cfg) - lateral = kgrid.y_vec - mask = falses(kgrid.Nx, kgrid.Ny) - @inbounds for i in 1:kgrid.Nx, j in 1:kgrid.Ny - value = ((depth[i] - lens_center_depth) / lens_axial_radius)^2 + - ((lateral[j] - lens_center_lateral) / lens_lateral_radius)^2 - if value <= 1.0 - mask[i, j] = true - c[i, j] = Float32(c_aberrator) - rho[i, j] = Float32(rho_aberrator) - end - end - - return c, rho, Dict{Symbol, Any}( - :aberrator => :lens, - :mask => mask, - :lens_center_depth => Float64(lens_center_depth), - :lens_center_lateral => Float64(lens_center_lateral), - :lens_axial_radius => Float64(lens_axial_radius), - :lens_lateral_radius => Float64(lens_lateral_radius), - :c_aberrator => Float64(c_aberrator), - :rho_aberrator => Float64(rho_aberrator), - ) + error("Unknown PAM medium aberrator: $aberrator") end diff --git a/src/pam/2d/plots.jl b/src/pam/2d/plots.jl new file mode 100644 index 0000000..a311db8 --- /dev/null +++ b/src/pam/2d/plots.jl @@ -0,0 +1,203 @@ +function map_db(map::AbstractMatrix{<:Real}, ref::Real) + safe_ref = max(Float64(ref), eps(Float64)) + return 10 .* log10.(max.(Float64.(map), eps(Float64)) ./ safe_ref) +end + +function map_norm(map::AbstractMatrix{<:Real}, ref::Real) + safe_ref = max(Float64(ref), eps(Float64)) + return Float64.(map) ./ safe_ref +end + +function source_pairs_mm(sources) + return [(src.depth * 1e3, src.lateral * 1e3) for src in sources] +end + +function scatter_sources!(ax, sources; color=:red, marker=nothing, markersize=nothing, strokewidth=2) + truth = source_pairs_mm(sources) + marker = isnothing(marker) ? (length(sources) > 20 ? :circle : :x) : marker + markersize = isnothing(markersize) ? (length(sources) > 20 ? 4 : 14) : markersize + scatter!( + ax, + last.(truth), + first.(truth); + color=color, + marker=marker, + markersize=markersize, + strokewidth=strokewidth, + ) +end + +function summary_line(stats) + if haskey(stats, :mean_radial_error_mm) + return "mean err=$(round(stats[:mean_radial_error_mm]; digits=2)) mm, success=$(get(stats, :num_success, 0))/$(get(stats, :num_truth_sources, 0))" + elseif haskey(stats, :f1) + return "F1=$(round(stats[:f1]; digits=3)), precision=$(round(stats[:precision]; digits=3)), recall=$(round(stats[:recall]; digits=3))" + end + return string(stats) +end + +function overlay_skull_2d!(ax, c, xvals, yvals; transpose_matrix=true) + skull_mask = skull_mask_from_c_columnwise(c; mask_outside=false) + any(skull_mask) || return nothing + c_max = Float64(maximum(c[skull_mask])) + overlay = fill(NaN, size(c)) + overlay[skull_mask] .= Float64.(c[skull_mask]) ./ c_max + mat = transpose_matrix ? overlay' : overlay + heatmap!(ax, xvals, yvals, mat; colormap=:grays, alpha=0.35, colorrange=(0, 1), nan_color=CairoMakie.RGBAf(0, 0, 0, 0)) + return nothing +end + +function lines_centerlines!(ax, centerlines; color=(:black, 0.45), linewidth=2) + isnothing(centerlines) && return nothing + for line in centerlines + length(line) >= 2 || continue + lines!(ax, [point[2] * 1e3 for point in line], [point[1] * 1e3 for point in line]; color=color, linewidth=linewidth) + end + return nothing +end + +function save_overview(path, c, rf, pam_geo, pam_hasa, kgrid, cfg, sources, stats_geo, stats_hasa) + depth_mm = depth_coordinates(kgrid, cfg) .* 1e3 + lateral_mm = kgrid.y_vec .* 1e3 + time_us = collect(0:(size(rf, 2) - 1)) .* cfg.dt .* 1e6 + map_ref = max(maximum(Float64.(pam_geo)), maximum(Float64.(pam_hasa)), eps(Float64)) + + fig = Figure(size=(1500, 1000)) + ax_medium = Axis(fig[1, 1]; title="Simulation Medium", xlabel="Lateral position [mm]", ylabel="Depth below receiver [mm]", aspect=DataAspect()) + hm_medium = heatmap!(ax_medium, lateral_mm, depth_mm, Float64.(c)'; colormap=:thermal) + hlines!(ax_medium, [0.0]; color=:white, linestyle=:dash) + scatter_sources!(ax_medium, sources) + Colorbar(fig[1, 2], hm_medium; label="Sound speed [m/s]") + + ax_rf = Axis(fig[1, 3]; title="Recorded RF Data", xlabel="Time [us]", ylabel="Lateral position [mm]") + rf_ref = max(maximum(abs.(rf)), eps(Float64)) + hm_rf = heatmap!(ax_rf, time_us, lateral_mm, Float64.(rf ./ rf_ref)'; colormap=:balance, colorrange=(-1, 1)) + Colorbar(fig[1, 4], hm_rf; label="Norm. pressure") + + ax_geo = Axis(fig[2, 1]; title="Geometric ASA PAM", xlabel="Lateral position [mm]", ylabel="Depth below receiver [mm]", aspect=DataAspect()) + hm_geo = heatmap!(ax_geo, lateral_mm, depth_mm, map_db(pam_geo, map_ref)'; colormap=:viridis, colorrange=(-30, 0)) + overlay_skull_2d!(ax_geo, c, lateral_mm, depth_mm) + scatter_sources!(ax_geo, sources) + Colorbar(fig[2, 2], hm_geo; label="dB") + + ax_hasa = Axis(fig[2, 3]; title="Corrected HASA PAM", xlabel="Lateral position [mm]", ylabel="Depth below receiver [mm]", aspect=DataAspect()) + hm_hasa = heatmap!(ax_hasa, lateral_mm, depth_mm, map_db(pam_hasa, map_ref)'; colormap=:viridis, colorrange=(-30, 0)) + overlay_skull_2d!(ax_hasa, c, lateral_mm, depth_mm) + scatter_sources!(ax_hasa, sources) + Colorbar(fig[2, 4], hm_hasa; label="dB") + + metrics_text = join([ + "Geometric: $(summary_line(stats_geo))", + "Corrected: $(summary_line(stats_hasa))", + ], "\n") + Label(fig[3, 1:4], metrics_text; tellwidth=false, halign=:left) + save(path, fig) +end + +function add_threshold_panel!( + fig, + row, + title, + intensity, + kgrid, + cfg, + sources; + threshold_ratios, + colors, + global_ref, + truth_mask, + truth_centerlines, + c=nothing, +) + depth_mm = depth_coordinates(kgrid, cfg) .* 1e3 + lateral_mm = kgrid.y_vec .* 1e3 + ax = Axis(fig[row, 1]; title=title, xlabel="Lateral [mm]", ylabel="Depth [mm]", aspect=DataAspect()) + hm = heatmap!(ax, lateral_mm, depth_mm, Float64.(intensity ./ global_ref)'; colormap=:viridis, colorrange=(0, 1)) + !isnothing(c) && overlay_skull_2d!(ax, c, lateral_mm, depth_mm) + if !isnothing(truth_mask) && any(truth_mask) && any(.!truth_mask) + contour!(ax, lateral_mm, depth_mm, Float64.(truth_mask)'; levels=[0.5], color=(:white, 0.85), linewidth=2.3, linestyle=:dash) + end + lines_centerlines!(ax, truth_centerlines; color=(:white, 0.7), linewidth=1.3) + local_ref = max(maximum(Float64.(intensity)), eps(Float64)) + for (idx, ratio) in pairs(threshold_ratios) + contour!(ax, lateral_mm, depth_mm, Float64.(intensity .>= ratio * local_ref)'; levels=[0.5], color=colors[idx], linewidth=2) + end + scatter_sources!(ax, sources; color=(:white, 0.55), marker=:circle, markersize=2.5, strokewidth=0) + return hm +end + +function add_threshold_table!(fig, row, col, title, stats) + gl = GridLayout(fig[row, col]; tellwidth=false, tellheight=true) + Label(gl[1, 1:4], title; font="DejaVu Sans Mono", fontsize=13, halign=:left, tellwidth=false) + headers = ["thr", "F1", "Prec", "Recall"] + for (c, h) in enumerate(headers) + Label(gl[2, c], h; font="DejaVu Sans Mono", fontsize=11, halign=:center) + end + for (r, entry) in enumerate(stats) + vals = [ + @sprintf("%.2f", Float64(entry[:threshold_ratio])), + @sprintf("%.3f", Float64(entry[:f1])), + @sprintf("%.3f", Float64(entry[:precision])), + @sprintf("%.3f", Float64(entry[:recall])), + ] + for (c, v) in enumerate(vals) + Label(gl[2 + r, c], v; font="DejaVu Sans Mono", fontsize=11, halign=:center) + end + end + colgap!(gl, 10) + rowgap!(gl, 2) +end + +function save_threshold_boundary_detection(path, pam_geo, pam_hasa, kgrid, cfg, sources; threshold_ratios, truth_radius, truth_mask, truth_centerlines, frequencies, c=nothing) + global_ref = max(maximum(Float64.(pam_geo)), maximum(Float64.(pam_hasa)), eps(Float64)) + colors = [:red, :orange, :cyan, :magenta, :lime] + while length(colors) < length(threshold_ratios) + append!(colors, colors) + end + geo_stats = threshold_detection_stats(pam_geo, kgrid, cfg, sources; threshold_ratios=threshold_ratios, truth_radius=truth_radius, truth_mask=truth_mask, frequencies=frequencies) + hasa_stats = threshold_detection_stats(pam_hasa, kgrid, cfg, sources; threshold_ratios=threshold_ratios, truth_radius=truth_radius, truth_mask=truth_mask, frequencies=frequencies) + + fig = Figure(size=(1000, 1300)) + hm = add_threshold_panel!( + fig, + 1, + "Uncorrected activity regions", + pam_geo, + kgrid, + cfg, + sources; + threshold_ratios=threshold_ratios, + colors=colors, + global_ref=global_ref, + truth_mask=truth_mask, + truth_centerlines=truth_centerlines, + c=c, + ) + add_threshold_panel!( + fig, + 2, + "Corrected activity regions", + pam_hasa, + kgrid, + cfg, + sources; + threshold_ratios=threshold_ratios, + colors=colors, + global_ref=global_ref, + truth_mask=truth_mask, + truth_centerlines=truth_centerlines, + c=c, + ) + Colorbar(fig[1:2, 2], hm; label="Norm. PAM intensity") + legend_elements = [LineElement(color=colors[i], linewidth=3) for i in eachindex(threshold_ratios)] + legend_labels = ["thr=$(round(r; digits=2))" for r in threshold_ratios] + Legend(fig[3, 1], legend_elements, legend_labels; orientation=:horizontal, tellheight=true, framevisible=false) + add_threshold_table!(fig, 4, 1, "Uncorrected quantitative region metrics", geo_stats) + add_threshold_table!(fig, 5, 1, "Corrected quantitative region metrics", hasa_stats) + save(path, fig) + return Dict( + "threshold_ratios" => threshold_ratios, + "geometric" => [string_key_dict(stats) for stats in geo_stats], + "hasa" => [string_key_dict(stats) for stats in hasa_stats], + ) +end diff --git a/src/pam/reconstruction.jl b/src/pam/2d/reconstruction.jl similarity index 100% rename from src/pam/reconstruction.jl rename to src/pam/2d/reconstruction.jl diff --git a/src/pam/sources.jl b/src/pam/2d/sources.jl similarity index 79% rename from src/pam/sources.jl rename to src/pam/2d/sources.jl index 10be7b7..1cd5d3b 100644 --- a/src/pam/sources.jl +++ b/src/pam/2d/sources.jl @@ -15,7 +15,6 @@ Base.@kwdef struct BubbleCluster2D <: EmissionSource2D lateral::Float64 fundamental::Float64 = 5e5 amplitude::Float64 = 1.0 - n_bubbles::Float64 = 1.0 harmonics::Vector{Int} = [2, 3] harmonic_amplitudes::Vector{Float64} = [1.0, 0.6] harmonic_phases::Vector{Float64} = [0.0, 0.0] @@ -24,38 +23,14 @@ Base.@kwdef struct BubbleCluster2D <: EmissionSource2D delay::Float64 = 0.0 end -Base.@kwdef struct GaussianPulseCluster2D <: EmissionSource2D - depth::Float64 - lateral::Float64 - fundamental::Float64 = 5e5 - amplitude::Float64 = 1.0 - n_bubbles::Float64 = 1.0 - harmonics::Vector{Int} = [2, 3] - harmonic_amplitudes::Vector{Float64} = [1.0, 0.6] - harmonic_phases::Vector{Float64} = [0.0, 0.0] - gate_duration::Float64 = 10e-6 - taper_ratio::Float64 = 0.25 - delay::Float64 = 0.0 -end - Base.@kwdef struct SourceVariabilityConfig frequency_jitter_fraction::Float64 = 0.0 end _emission_frequencies(src::PointSource2D) = Float64[src.frequency] _emission_frequencies(src::BubbleCluster2D) = Float64[n * src.fundamental for n in src.harmonics] -_emission_frequencies(src::GaussianPulseCluster2D) = Float64[n * src.fundamental for n in src.harmonics] emission_frequencies(src::EmissionSource2D) = _emission_frequencies(src) -cavitation_model(::BubbleCluster2D) = :harmonic_cos -cavitation_model(::GaussianPulseCluster2D) = :gaussian_pulse - -function _normalize_cavitation_model(cavitation_model) - model = Symbol(replace(lowercase(string(cavitation_model)), "-" => "_")) - model in (:harmonic_cos, :gaussian_pulse) || - error("Unknown cavitation_model: $cavitation_model (expected harmonic-cos or gaussian-pulse).") - return model -end function _normalize_cluster_phase_mode(phase_mode) mode = Symbol(replace(lowercase(string(phase_mode)), "-" => "_")) @@ -67,10 +42,10 @@ end function _normalize_source_phase_mode(source_phase_mode) mode = Symbol(replace(lowercase(string(source_phase_mode)), "-" => "_")) - mode in (:coherent, :random_static_phase, :random_phase_per_window, :random_phase_per_realization) || + mode in (:coherent, :random_static_phase, :random_phase_per_window) || error( "Unknown source_phase_mode: $source_phase_mode. " * - "Expected: coherent, random_static_phase, random_phase_per_window, or random_phase_per_realization.", + "Expected: coherent, random_static_phase, or random_phase_per_window.", ) return mode end @@ -251,10 +226,8 @@ function make_squiggle_bubble_sources( lateral_bounds::Tuple{<:Real, <:Real}=(-Inf, Inf), fundamental::Real=5e5, amplitude::Real=1.0, - n_bubbles::Real=1.0, harmonics::AbstractVector{<:Integer}=[2, 3, 4], harmonic_amplitudes::AbstractVector{<:Real}=[1.0, 0.6, 0.3], - cavitation_model=:harmonic_cos, gate_duration::Real=50e-6, taper_ratio::Real=0.25, delay::Real=0.0, @@ -272,7 +245,6 @@ function make_squiggle_bubble_sources( length(harmonic_amplitudes_f) == length(harmonics_i) || error("harmonic_amplitudes must have the same length as harmonics.") mode = _normalize_cluster_phase_mode(phase_mode) - source_model = _normalize_cavitation_model(cavitation_model) clusters = EmissionSource2D[] all_centerlines = Vector{Tuple{Float64, Float64}}[] @@ -320,7 +292,6 @@ function make_squiggle_bubble_sources( lateral=lateral, fundamental=Float64(fundamental), amplitude=Float64(amplitude), - n_bubbles=Float64(n_bubbles), harmonics=copy(harmonics_i), harmonic_amplitudes=copy(harmonic_amplitudes_f), harmonic_phases=phases, @@ -328,7 +299,7 @@ function make_squiggle_bubble_sources( taper_ratio=Float64(taper_ratio), delay=Float64(delay), ) - push!(clusters, source_model == :gaussian_pulse ? GaussianPulseCluster2D(; kwargs...) : BubbleCluster2D(; kwargs...)) + push!(clusters, BubbleCluster2D(; kwargs...)) end end @@ -347,14 +318,12 @@ function make_squiggle_bubble_sources( :source_count_by_anchor => source_count_by_anchor, :centerlines => all_centerlines, :phase_mode => mode, - :cavitation_model => source_model, ) return clusters, meta end _source_duration(src::PointSource2D) = src.num_cycles / src.frequency _source_duration(src::BubbleCluster2D) = src.gate_duration -_source_duration(src::GaussianPulseCluster2D) = src.gate_duration function _tukey_window(n::Int, ratio::Real) n <= 0 && return Float64[] @@ -407,7 +376,6 @@ function _cluster_emission_signal(nt::Int, dt::Real, src::BubbleCluster2D) isempty(active) && return signal envelope = _tukey_window(length(active), src.taper_ratio) - total_amp = src.amplitude * src.n_bubbles t_active = t[active] accumulator = zeros(Float64, length(active)) @@ -417,44 +385,12 @@ function _cluster_emission_signal(nt::Int, dt::Real, src::BubbleCluster2D) phi_n = src.harmonic_phases[i] accumulator .+= alpha_n .* cos.(2pi .* n .* src.fundamental .* t_active .+ phi_n) end - signal[active] .= total_amp .* envelope .* accumulator - return signal -end - -function _cluster_emission_signal(nt::Int, dt::Real, src::GaussianPulseCluster2D) - length(src.harmonics) == length(src.harmonic_amplitudes) || - error("GaussianPulseCluster2D: harmonics and harmonic_amplitudes must have equal length.") - length(src.harmonics) == length(src.harmonic_phases) || - error("GaussianPulseCluster2D: harmonics and harmonic_phases must have equal length.") - - signal = zeros(Float64, nt) - samples = collect(0:(nt - 1)) - t = samples .* Float64(dt) .- src.delay - active = findall((t .>= 0.0) .& (t .<= src.gate_duration)) - isempty(active) && return signal - - duration = Float64(src.gate_duration) - duration > 0 || return signal - center = duration / 2 - sigma = duration / 6 - t_active = t[active] - envelope = exp.(-0.5 .* ((t_active .- center) ./ sigma) .^ 2) - total_amp = src.amplitude * src.n_bubbles - - accumulator = zeros(Float64, length(active)) - @inbounds for i in eachindex(src.harmonics) - n = src.harmonics[i] - alpha_n = src.harmonic_amplitudes[i] - phi_n = src.harmonic_phases[i] - accumulator .+= alpha_n .* cos.(2pi .* n .* src.fundamental .* (t_active .- center) .+ phi_n) - end - signal[active] .= total_amp .* envelope .* accumulator + signal[active] .= src.amplitude .* envelope .* accumulator return signal end _source_signal(nt::Int, dt::Real, src::PointSource2D) = _tone_burst_signal(nt, dt, src) _source_signal(nt::Int, dt::Real, src::BubbleCluster2D) = _cluster_emission_signal(nt, dt, src) -_source_signal(nt::Int, dt::Real, src::GaussianPulseCluster2D) = _cluster_emission_signal(nt, dt, src) function _resample_source_phases( sources::AbstractVector{<:EmissionSource2D}, @@ -465,17 +401,7 @@ function _resample_source_phases( BubbleCluster2D( depth=src.depth, lateral=src.lateral, fundamental=src.fundamental, amplitude=src.amplitude, - n_bubbles=src.n_bubbles, harmonics=copy(src.harmonics), - harmonic_amplitudes=copy(src.harmonic_amplitudes), - harmonic_phases=2pi .* rand(rng, length(src.harmonics)), - gate_duration=src.gate_duration, taper_ratio=src.taper_ratio, - delay=src.delay, - ) - elseif src isa GaussianPulseCluster2D - GaussianPulseCluster2D( - depth=src.depth, lateral=src.lateral, - fundamental=src.fundamental, amplitude=src.amplitude, - n_bubbles=src.n_bubbles, harmonics=copy(src.harmonics), + harmonics=copy(src.harmonics), harmonic_amplitudes=copy(src.harmonic_amplitudes), harmonic_phases=2pi .* rand(rng, length(src.harmonics)), gate_duration=src.gate_duration, taper_ratio=src.taper_ratio, @@ -518,17 +444,7 @@ function _expand_sources_per_window( BubbleCluster2D( depth=src.depth, lateral=src.lateral, fundamental=src.fundamental * fscale, amplitude=src.amplitude, - n_bubbles=src.n_bubbles, harmonics=copy(src.harmonics), - harmonic_amplitudes=copy(src.harmonic_amplitudes), - harmonic_phases=2pi .* rand(rng, length(src.harmonics)), - gate_duration=min(src.gate_duration, frame_dur), taper_ratio=src.taper_ratio, - delay=d, - ) - elseif src isa GaussianPulseCluster2D - GaussianPulseCluster2D( - depth=src.depth, lateral=src.lateral, - fundamental=src.fundamental * fscale, amplitude=src.amplitude, - n_bubbles=src.n_bubbles, harmonics=copy(src.harmonics), + harmonics=copy(src.harmonics), harmonic_amplitudes=copy(src.harmonic_amplitudes), harmonic_phases=2pi .* rand(rng, length(src.harmonics)), gate_duration=min(src.gate_duration, frame_dur), taper_ratio=src.taper_ratio, diff --git a/src/pam/workflow.jl b/src/pam/2d/workflow.jl similarity index 72% rename from src/pam/workflow.jl rename to src/pam/2d/workflow.jl index ed28a14..cb5d6e6 100644 --- a/src/pam/workflow.jl +++ b/src/pam/2d/workflow.jl @@ -27,7 +27,7 @@ function _run_pam_per_window( sources::AbstractVector{<:EmissionSource2D}, cfg::PAMConfig; source_phase_mode::Symbol, - use_gpu::Bool, + kwave_use_gpu::Bool, rng::Random.AbstractRNG, recon_kwargs::NamedTuple, variability::SourceVariabilityConfig=SourceVariabilityConfig(), @@ -46,7 +46,7 @@ function _run_pam_per_window( accumulation=win_cfg.accumulation, ) eff_recon_kwargs = merge(recon_kwargs, (reconstruction_mode=:windowed, window_config=eff_window_config)) - rf, kgrid, sim_info = simulate_point_sources(c, rho, expanded, cfg; use_gpu=use_gpu) + rf, kgrid, sim_info = simulate_point_sources(c, rho, expanded, cfg; use_gpu=kwave_use_gpu) results = reconstruct_pam_case( rf, c, @@ -58,50 +58,10 @@ function _run_pam_per_window( ) results[:kgrid] = kgrid results[:source_phase_mode] = source_phase_mode - results[:n_realizations] = 1 results[:n_frames] = n_frames return results end -function _run_pam_multirealization( - c::AbstractMatrix{<:Real}, - rho::AbstractMatrix{<:Real}, - sources::AbstractVector{<:EmissionSource2D}, - cfg::PAMConfig; - n_realizations::Int, - source_phase_mode::Symbol, - use_gpu::Bool, - rng::Random.AbstractRNG, - recon_kwargs::NamedTuple, -) - geo_acc = nothing - hasa_acc = nothing - last_kgrid = nothing - last_results = nothing - - for _ in 1:n_realizations - resampled = _resample_source_phases(sources, rng) - rf, kgrid, sim_info = simulate_point_sources(c, rho, resampled, cfg; use_gpu=use_gpu) - results = reconstruct_pam_case(rf, c, resampled, cfg; simulation_info=sim_info, recon_kwargs...) - if isnothing(geo_acc) - geo_acc = Float64.(results[:pam_geo]) - hasa_acc = Float64.(results[:pam_hasa]) - else - geo_acc .+= results[:pam_geo] - hasa_acc .+= results[:pam_hasa] - end - last_kgrid = kgrid - last_results = results - end - - last_results[:pam_geo] = geo_acc ./ n_realizations - last_results[:pam_hasa] = hasa_acc ./ n_realizations - last_results[:kgrid] = last_kgrid - last_results[:source_phase_mode] = source_phase_mode - last_results[:n_realizations] = n_realizations - return last_results -end - function run_pam_case( c::AbstractMatrix{<:Real}, rho::AbstractMatrix{<:Real}, @@ -110,13 +70,8 @@ function run_pam_case( frequencies::Union{Nothing, AbstractVector{<:Real}}=nothing, bandwidth::Real=0.0, use_gpu::Bool=false, + kwave_use_gpu::Bool=false, analysis_mode::Symbol=:localization, - peak_method::Symbol=:argmax, - clean_loop_gain::Real=0.1, - clean_max_iter::Integer=500, - clean_threshold_ratio::Real=1e-2, - clean_psf_axial_fwhm::Union{Nothing, Real}=nothing, - clean_psf_lateral_fwhm::Union{Nothing, Real}=nothing, detection_truth_radius::Real=cfg.success_tolerance, detection_threshold_ratio::Real=0.2, detection_truth_mask::Union{Nothing, AbstractMatrix{Bool}}=nothing, @@ -124,7 +79,6 @@ function run_pam_case( reconstruction_mode::Symbol=:full, window_config::PAMWindowConfig=PAMWindowConfig(), source_phase_mode::Symbol=:coherent, - n_realizations::Int=1, rng::Random.AbstractRNG=Random.default_rng(), source_variability::SourceVariabilityConfig=SourceVariabilityConfig(), show_progress::Bool=false, @@ -138,12 +92,6 @@ function run_pam_case( frequencies=recon_freqs, bandwidth=bandwidth, analysis_mode=analysis_mode, - peak_method=peak_method, - clean_loop_gain=clean_loop_gain, - clean_max_iter=clean_max_iter, - clean_threshold_ratio=clean_threshold_ratio, - clean_psf_axial_fwhm=clean_psf_axial_fwhm, - clean_psf_lateral_fwhm=clean_psf_lateral_fwhm, detection_truth_radius=detection_truth_radius, detection_threshold_ratio=detection_threshold_ratio, detection_truth_mask=detection_truth_mask, @@ -155,31 +103,20 @@ function run_pam_case( benchmark=benchmark, window_batch=window_batch, ) - if phase_mode == :random_phase_per_realization - n_realizations >= 1 || error("n_realizations must be >= 1.") - return _run_pam_multirealization( - c, rho, effective_sources, cfg; - n_realizations=n_realizations, - source_phase_mode=phase_mode, - use_gpu=use_gpu, - rng=rng, - recon_kwargs=recon_kwargs, - ) - elseif phase_mode == :random_phase_per_window + if phase_mode == :random_phase_per_window return _run_pam_per_window( c, rho, effective_sources, cfg; source_phase_mode=phase_mode, - use_gpu=use_gpu, + kwave_use_gpu=kwave_use_gpu, rng=rng, recon_kwargs=recon_kwargs, variability=source_variability, ) end - rf, kgrid, sim_info = simulate_point_sources(c, rho, effective_sources, cfg; use_gpu=use_gpu) + rf, kgrid, sim_info = simulate_point_sources(c, rho, effective_sources, cfg; use_gpu=kwave_use_gpu) results = reconstruct_pam_case(rf, c, effective_sources, cfg; simulation_info=sim_info, recon_kwargs...) results[:kgrid] = kgrid results[:source_phase_mode] = phase_mode - results[:n_realizations] = 1 return results end @@ -196,12 +133,6 @@ function reconstruct_pam_case( frequencies::Union{Nothing, AbstractVector{<:Real}}=nothing, bandwidth::Real=0.0, analysis_mode::Symbol=:localization, - peak_method::Symbol=:argmax, - clean_loop_gain::Real=0.1, - clean_max_iter::Integer=500, - clean_threshold_ratio::Real=1e-2, - clean_psf_axial_fwhm::Union{Nothing, Real}=nothing, - clean_psf_lateral_fwhm::Union{Nothing, Real}=nothing, detection_truth_radius::Real=cfg.success_tolerance, detection_threshold_ratio::Real=0.2, detection_truth_mask::Union{Nothing, AbstractMatrix{Bool}}=nothing, @@ -269,18 +200,9 @@ function reconstruct_pam_case( isempty(truth_sources) && error("At least one analysis source is required.") stats_geo, stats_hasa = if analysis_mode == :localization - analyse_kwargs = ( - peak_method=peak_method, - frequencies=recon_freqs, - clean_loop_gain=clean_loop_gain, - clean_max_iter=clean_max_iter, - clean_threshold_ratio=clean_threshold_ratio, - clean_psf_axial_fwhm=clean_psf_axial_fwhm, - clean_psf_lateral_fwhm=clean_psf_lateral_fwhm, - ) ( - analyse_pam_2d(pam_geo, kgrid, cfg, truth_sources; analyse_kwargs...), - analyse_pam_2d(pam_hasa, kgrid, cfg, truth_sources; analyse_kwargs...), + analyse_pam_2d(pam_geo, kgrid, cfg, truth_sources), + analyse_pam_2d(pam_hasa, kgrid, cfg, truth_sources), ) elseif analysis_mode == :detection analyse_kwargs = ( @@ -288,8 +210,6 @@ function reconstruct_pam_case( threshold_ratio=detection_threshold_ratio, truth_mask=detection_truth_mask, frequencies=recon_freqs, - psf_axial_fwhm=clean_psf_axial_fwhm, - psf_lateral_fwhm=clean_psf_lateral_fwhm, ) ( analyse_pam_detection_2d(pam_geo, kgrid, cfg, truth_sources; analyse_kwargs...), diff --git a/src/pam/3d/analysis3d.jl b/src/pam/3d/analysis3d.jl new file mode 100644 index 0000000..4aecef7 --- /dev/null +++ b/src/pam/3d/analysis3d.jl @@ -0,0 +1,305 @@ +function find_pam_peaks_3d( + intensity::AbstractArray{<:Real, 3}, + ::NamedTuple, + cfg::PAMConfig3D; + n_peaks::Integer, + suppression_radius::Real=cfg.peak_suppression_radius, +) + work = copy(Float64.(intensity)) + row_start = receiver_row(cfg) + 1 + pml_guard = _pam_pml_guard_3d(cfg) + row_stop = max(row_start, size(work, 1) - pml_guard) + row_start <= row_stop || error("No valid reconstruction rows remain after excluding the receiver row and PML.") + work[1:(row_start - 1), :, :] .= -Inf + if row_stop < size(work, 1) + work[(row_stop + 1):end, :, :] .= -Inf + end + # Suppress the outermost lateral cell on each edge (aperture boundary fold-back). + work[:, 1, :] .= -Inf + work[:, end, :] .= -Inf + work[:, :, 1] .= -Inf + work[:, :, end] .= -Inf + + rad_rows = max(1, round(Int, suppression_radius / cfg.dx)) + rad_y = max(1, round(Int, suppression_radius / cfg.dy)) + rad_z = max(1, round(Int, suppression_radius / cfg.dz)) + peaks = NTuple{3, Int}[] + + for _ in 1:Int(n_peaks) + idx = Tuple(argmax(work)) + isfinite(work[idx...]) || break + push!(peaks, idx) + r0, y0, z0 = idx + work[max(1, r0 - rad_rows):min(size(work, 1), r0 + rad_rows), + max(1, y0 - rad_y):min(size(work, 2), y0 + rad_y), + max(1, z0 - rad_z):min(size(work, 3), z0 + rad_z)] .= -Inf + end + + return peaks +end + +function pam_truth_mask_3d( + sources::AbstractVector{<:EmissionSource3D}, + grid::NamedTuple, + cfg::PAMConfig3D; + radius::Real=cfg.success_tolerance, +) + radius_m = Float64(radius) + radius_m >= 0 || error("truth-mask radius must be non-negative.") + nx, ny, nz = pam_Nx(cfg), pam_Ny(cfg), pam_Nz(cfg) + mask = falses(nx, ny, nz) + x = collect(grid.x) + y = collect(grid.y) + z = collect(grid.z) + r0 = receiver_row(cfg) + radius2 = radius_m^2 + row_r = ceil(Int, radius_m / cfg.dx) + col_r_y = ceil(Int, radius_m / cfg.dy) + col_r_z = ceil(Int, radius_m / cfg.dz) + + for src in sources + depth = src.depth + src_x = x[r0] + depth + row0 = r0 + round(Int, depth / cfg.dx) + col0_y = argmin(abs.(y .- src.lateral_y)) + col0_z = argmin(abs.(z .- src.lateral_z)) + for row in max(r0 + 1, row0 - row_r):min(nx, row0 + row_r) + dx2 = (x[row] - src_x)^2 + for iy in max(1, col0_y - col_r_y):min(ny, col0_y + col_r_y) + dy2 = (y[iy] - src.lateral_y)^2 + for iz in max(1, col0_z - col_r_z):min(nz, col0_z + col_r_z) + dz2 = (z[iz] - src.lateral_z)^2 + if dx2 + dy2 + dz2 <= radius2 + mask[row, iy, iz] = true + end + end + end + end + end + return mask +end + +function source_detection_stats_3d(pred, grid, cfg::PAMConfig3D, sources; radius::Real) + radius_m = Float64(radius) + radius_m >= 0 || error("source detection radius must be non-negative.") + isempty(sources) && return Dict{Symbol, Any}( + :source_recall => 0.0, + :detected_source_count => 0, + :num_truth_sources => 0, + :mean_detected_source_distance_mm => nothing, + :max_detected_source_distance_mm => nothing, + ) + x = collect(grid.x) + y = collect(grid.y) + z = collect(grid.z) + r0 = receiver_row(cfg) + row_r = ceil(Int, radius_m / cfg.dx) + col_r_y = ceil(Int, radius_m / cfg.dy) + col_r_z = ceil(Int, radius_m / cfg.dz) + radius2 = radius_m^2 + + detected = 0 + distances_mm = Float64[] + for src in sources + src_x = x[r0] + src.depth + row0 = r0 + round(Int, src.depth / cfg.dx) + col0_y = argmin(abs.(y .- src.lateral_y)) + col0_z = argmin(abs.(z .- src.lateral_z)) + best_d2 = Inf + for row in max(r0 + 1, row0 - row_r):min(size(pred, 1), row0 + row_r) + dx2 = (x[row] - src_x)^2 + for iy in max(1, col0_y - col_r_y):min(size(pred, 2), col0_y + col_r_y) + dy2 = (y[iy] - src.lateral_y)^2 + for iz in max(1, col0_z - col_r_z):min(size(pred, 3), col0_z + col_r_z) + pred[row, iy, iz] || continue + d2 = dx2 + dy2 + (z[iz] - src.lateral_z)^2 + if d2 <= radius2 && d2 < best_d2 + best_d2 = d2 + end + end + end + end + if isfinite(best_d2) + detected += 1 + push!(distances_mm, sqrt(best_d2) * 1e3) + end + end + + return Dict{Symbol, Any}( + :source_recall => detected / length(sources), + :detected_source_count => detected, + :num_truth_sources => length(sources), + :mean_detected_source_distance_mm => isempty(distances_mm) ? nothing : mean(distances_mm), + :max_detected_source_distance_mm => isempty(distances_mm) ? nothing : maximum(distances_mm), + ) +end + +function threshold_detection_stats_3d(intensity, grid, cfg, sources; threshold_ratios, truth_radius, truth_mask) + truth = isnothing(truth_mask) ? pam_truth_mask_3d(sources, grid, cfg; radius=truth_radius) : truth_mask + local_ref = max(maximum(Float64.(intensity)), eps(Float64)) + return [ + begin + pred = intensity .>= ratio * local_ref + tp = count(pred .& truth) + fp = count(pred .& .!truth) + fn = count(.!pred .& truth) + precision = tp + fp == 0 ? 0.0 : tp / (tp + fp) + recall = tp + fn == 0 ? 0.0 : tp / (tp + fn) + f1 = precision + recall == 0 ? 0.0 : 2 * precision * recall / (precision + recall) + source_stats = source_detection_stats_3d(pred, grid, cfg, sources; radius=truth_radius) + source_recall = Float64(source_stats[:source_recall]) + source_f1 = precision + source_recall == 0 ? 0.0 : 2 * precision * source_recall / (precision + source_recall) + merge(Dict( + :threshold_ratio => ratio, + :f1 => source_f1, + :source_f1 => source_f1, + :voxel_f1 => f1, + :precision => precision, + :recall => source_recall, + :voxel_recall => recall, + :true_positive_voxels => tp, + :false_positive_voxels => fp, + :false_negative_voxels => fn, + :predicted_voxels => count(pred), + :truth_voxels => count(truth), + ), source_stats) + end + for ratio in threshold_ratios + ] +end + +function best_threshold_entry_3d(stats) + isempty(stats) && error("No 3D threshold stats available.") + best = first(stats) + for entry in stats[2:end] + score_metric = haskey(entry, :source_f1) ? :source_f1 : :f1 + best_metric = haskey(best, :source_f1) ? :source_f1 : :f1 + score = (Float64(entry[score_metric]), Float64(entry[:precision]), Float64(entry[:threshold_ratio])) + best_score = (Float64(best[best_metric]), Float64(best[:precision]), Float64(best[:threshold_ratio])) + if score > best_score + best = entry + end + end + return best +end + +_metric_value(entry, key::Symbol, fallback::Symbol=key) = Float64(get(entry, key, entry[fallback])) + +function _argmax_by(entries, scorefn) + best = first(entries) + best_score = scorefn(best) + for entry in entries[2:end] + score = scorefn(entry) + if score > best_score + best = entry + best_score = score + end + end + return best +end + +function _threshold_tradeoff_entry_3d(stats, best, target::Symbol) + best_f1 = _metric_value(best, :source_f1, :f1) + best_value = _metric_value(best, target, target == :source_recall ? :recall : target) + metric_key = target == :source_recall ? :source_recall : :precision + fallback_key = target == :source_recall ? :recall : :precision + candidates = [entry for entry in stats if _metric_value(entry, metric_key, fallback_key) > best_value + 1e-9] + for floor_fraction in (0.95, 0.90, 0.0) + viable = [entry for entry in candidates if _metric_value(entry, :source_f1, :f1) >= floor_fraction * best_f1] + isempty(viable) && continue + if target == :source_recall + return _argmax_by(viable, entry -> ( + _metric_value(entry, :source_recall, :recall), + _metric_value(entry, :source_f1, :f1), + _metric_value(entry, :precision), + )) + else + return _argmax_by(viable, entry -> ( + _metric_value(entry, :precision), + _metric_value(entry, :source_f1, :f1), + _metric_value(entry, :source_recall, :recall), + )) + end + end + return best +end + +function threshold_outline_entries_3d(stats) + best = best_threshold_entry_3d(stats) + recall = _threshold_tradeoff_entry_3d(stats, best, :source_recall) + precision = _threshold_tradeoff_entry_3d(stats, best, :precision) + return [ + (kind=:best_f1, label="best F1", color=:cyan, entry=best), + (kind=:more_recall, label="more recall", color=:lime, entry=recall), + (kind=:more_precision, label="more precision", color=:magenta, entry=precision), + ] +end + +function analyse_pam_3d( + intensity::AbstractArray{<:Real, 3}, + grid::NamedTuple, + cfg::PAMConfig3D, + sources::AbstractVector{<:EmissionSource3D}; + n_peaks::Union{Nothing, Integer}=nothing, + success_tolerance::Real=cfg.success_tolerance, + suppression_radius::Real=cfg.peak_suppression_radius, +) + n_truth = length(sources) + n_truth > 0 || error("At least one emission source is required for 3D PAM analysis.") + n_find = isnothing(n_peaks) ? n_truth : Int(n_peaks) + peaks = find_pam_peaks_3d(intensity, grid, cfg; n_peaks=n_find, suppression_radius=suppression_radius) + length(peaks) == n_truth || error("Expected to recover $n_truth peaks, found $(length(peaks)).") + + x = collect(grid.x) + y = collect(grid.y) + z = collect(grid.z) + rr = receiver_row(cfg) + + truth_mm = [(src.depth * 1e3, src.lateral_y * 1e3, src.lateral_z * 1e3) for src in sources] + pred_mm = [((x[idx[1]] - x[rr]) * 1e3, y[idx[2]] * 1e3, z[idx[3]] * 1e3) for idx in peaks] + + cost = Matrix{Float64}(undef, n_truth, n_truth) + for i in 1:n_truth, j in 1:n_truth + d_ax = truth_mm[i][1] - pred_mm[j][1] + d_lat_y = truth_mm[i][2] - pred_mm[j][2] + d_lat_z = truth_mm[i][3] - pred_mm[j][3] + cost[i, j] = sqrt(d_ax^2 + d_lat_y^2 + d_lat_z^2) + end + assignment, _ = _best_assignment(cost) + + matched_pred_mm = [pred_mm[assignment[i]] for i in 1:n_truth] + matched_indices = [peaks[assignment[i]] for i in 1:n_truth] + axial_errors_mm = [truth_mm[i][1] - matched_pred_mm[i][1] for i in 1:n_truth] + lateral_y_errors_mm = [truth_mm[i][2] - matched_pred_mm[i][2] for i in 1:n_truth] + lateral_z_errors_mm = [truth_mm[i][3] - matched_pred_mm[i][3] for i in 1:n_truth] + radial_errors_mm = [sqrt(axial_errors_mm[i]^2 + lateral_y_errors_mm[i]^2 + lateral_z_errors_mm[i]^2) for i in 1:n_truth] + + raw_peak_intensities = [Float64(intensity[idx...]) for idx in matched_indices] + max_intensity = max(maximum(Float64.(intensity)), eps(Float64)) + norm_peak_intensities = raw_peak_intensities ./ max_intensity + + tol_mm = Float64(success_tolerance) * 1e3 + successes = radial_errors_mm .<= tol_mm + num_success = count(identity, successes) + + return Dict{Symbol, Any}( + :truth_mm => truth_mm, + :predicted_mm => matched_pred_mm, + :peak_indices => matched_indices, + :axial_errors_mm => axial_errors_mm, + :lateral_y_errors_mm => lateral_y_errors_mm, + :lateral_z_errors_mm => lateral_z_errors_mm, + :radial_errors_mm => radial_errors_mm, + :mean_axial_error_mm => mean(abs.(axial_errors_mm)), + :mean_lateral_y_error_mm => mean(abs.(lateral_y_errors_mm)), + :mean_lateral_z_error_mm => mean(abs.(lateral_z_errors_mm)), + :mean_radial_error_mm => mean(radial_errors_mm), + :max_radial_error_mm => maximum(radial_errors_mm), + :success_tolerance_mm => tol_mm, + :success_rate => num_success / n_truth, + :num_success => num_success, + :raw_peak_intensities => raw_peak_intensities, + :norm_peak_intensities => norm_peak_intensities, + :mean_norm_peak_intensity => mean(norm_peak_intensities), + ) +end diff --git a/src/pam/config3d.jl b/src/pam/3d/config3d.jl similarity index 100% rename from src/pam/config3d.jl rename to src/pam/3d/config3d.jl diff --git a/src/pam/medium3d.jl b/src/pam/3d/medium3d.jl similarity index 100% rename from src/pam/medium3d.jl rename to src/pam/3d/medium3d.jl diff --git a/src/pam/3d/plots3d.jl b/src/pam/3d/plots3d.jl new file mode 100644 index 0000000..e6c5f8e --- /dev/null +++ b/src/pam/3d/plots3d.jl @@ -0,0 +1,409 @@ +function source_triples_mm(sources::AbstractVector{<:EmissionSource3D}) + return [(src.depth * 1e3, src.lateral_y * 1e3, src.lateral_z * 1e3) for src in sources] +end + +function _c_slice_for_projection(c::AbstractArray{<:Real, 3}, projection::Symbol) + if projection == :depth_y + return dropdims(maximum(c; dims=3), dims=3) + elseif projection == :depth_z + return dropdims(maximum(c; dims=2), dims=2) + else # :y_z + return dropdims(maximum(c; dims=1), dims=1) + end +end + +function overlay_skull_3d_projection!(ax, c::AbstractArray{<:Real, 3}, xvals, yvals, projection::Symbol) + c2d = _c_slice_for_projection(c, projection) + # :y_z projection is not transposed (matches _projection_heatmap_matrix_3d convention). + overlay_skull_2d!(ax, c2d, xvals, yvals; transpose_matrix=(projection != :y_z)) + return nothing +end + +function _project3d_values(intensity::AbstractArray{<:Real, 3}, projection::Symbol) + values = Float64.(intensity) + if projection == :depth_y + return dropdims(maximum(values; dims=3), dims=3) + elseif projection == :depth_z + return dropdims(maximum(values; dims=2), dims=2) + elseif projection == :y_z + return dropdims(maximum(values; dims=1), dims=1) + end + error("Unknown 3D projection: $projection") +end + +function _project3d_mask(mask::AbstractArray{Bool, 3}, projection::Symbol) + if projection == :depth_y + return dropdims(any(mask; dims=3), dims=3) + elseif projection == :depth_z + return dropdims(any(mask; dims=2), dims=2) + elseif projection == :y_z + return dropdims(any(mask; dims=1), dims=1) + end + error("Unknown 3D projection: $projection") +end + +function _projection_axes_3d(grid, cfg::PAMConfig3D, projection::Symbol) + depth_mm = depth_coordinates_3d(cfg) .* 1e3 + y_mm = collect(grid.y) .* 1e3 + z_mm = collect(grid.z) .* 1e3 + if projection == :depth_y + return y_mm, depth_mm, "Y [mm]", "Depth [mm]" + elseif projection == :depth_z + return z_mm, depth_mm, "Z [mm]", "Depth [mm]" + elseif projection == :y_z + return y_mm, z_mm, "Y [mm]", "Z [mm]" + end + error("Unknown 3D projection: $projection") +end + +function _projection_heatmap_matrix_3d(values::AbstractMatrix, projection::Symbol) + projection == :y_z && return values + return values' +end + +function scatter_sources_3d_projection!(ax, sources, projection::Symbol; color=(:white, 0.75)) + truth = source_triples_mm(sources) + if projection == :depth_y + scatter!(ax, [t[2] for t in truth], [t[1] for t in truth]; color=color, marker=:x, markersize=13, strokewidth=2) + elseif projection == :depth_z + scatter!(ax, [t[3] for t in truth], [t[1] for t in truth]; color=color, marker=:x, markersize=13, strokewidth=2) + elseif projection == :y_z + scatter!(ax, [t[2] for t in truth], [t[3] for t in truth]; color=color, marker=:x, markersize=13, strokewidth=2) + end + return nothing +end + +function add_projection_panel_3d!( + fig, + row, + col, + title, + intensity, + truth_mask, + grid, + cfg, + sources; + projection::Symbol, + outline_entries, + global_ref, + c=nothing, +) + xvals, yvals, xlabel, ylabel = _projection_axes_3d(grid, cfg, projection) + proj = _project3d_values(intensity, projection) + truth_proj = _project3d_mask(truth_mask, projection) + ax = Axis(fig[row, col]; title=title, xlabel=xlabel, ylabel=ylabel, aspect=DataAspect()) + hm = heatmap!( + ax, + xvals, + yvals, + _projection_heatmap_matrix_3d(map_norm(proj, global_ref), projection); + colormap=:viridis, + colorrange=(0, 1), + ) + !isnothing(c) && overlay_skull_3d_projection!(ax, c, xvals, yvals, projection) + if any(truth_proj) && any(.!truth_proj) + contour!( + ax, + xvals, + yvals, + _projection_heatmap_matrix_3d(Float64.(truth_proj), projection); + levels=[0.5], + color=(:white, 0.85), + linewidth=2.4, + linestyle=:dash, + ) + end + local_ref = max(maximum(Float64.(intensity)), eps(Float64)) + for outline in outline_entries + ratio = Float64(outline.entry[:threshold_ratio]) + pred_proj = _project3d_mask(intensity .>= ratio * local_ref, projection) + if any(pred_proj) && any(.!pred_proj) + contour!( + ax, + xvals, + yvals, + _projection_heatmap_matrix_3d(Float64.(pred_proj), projection); + levels=[0.5], + color=outline.color, + linewidth=2, + ) + end + end + scatter_sources_3d_projection!(ax, sources, projection) + return hm +end + +function add_threshold_table_3d!(fig, row, col, title, stats; outline_entries=nothing) + rows_data = isnothing(outline_entries) ? [(label="", entry=entry) for entry in stats] : + [(label=outline.label, entry=outline.entry) for outline in outline_entries] + gl = GridLayout(fig[row, col]; tellwidth=false, tellheight=true) + Label(gl[1, 1:8], title; font="DejaVu Sans Mono", fontsize=13, halign=:left, tellwidth=false) + headers = ["", "thr", "SrcF1", "Prec", "SrcRc", "VoxF1", "Vox"] + for (c, h) in enumerate(headers) + Label(gl[2, c], h; font="DejaVu Sans Mono", fontsize=11, halign=c == 1 ? :right : :center) + end + for (r, row_entry) in enumerate(rows_data) + entry = row_entry.entry + vals = [ + row_entry.label, + @sprintf("%.2f", Float64(entry[:threshold_ratio])), + @sprintf("%.3f", Float64(get(entry, :source_f1, entry[:f1]))), + @sprintf("%.3f", Float64(entry[:precision])), + @sprintf("%.3f", Float64(get(entry, :source_recall, entry[:recall]))), + @sprintf("%.3f", Float64(get(entry, :voxel_f1, entry[:f1]))), + @sprintf("%d", Int(entry[:predicted_voxels])), + ] + for (c, v) in enumerate(vals) + Label(gl[2 + r, c], v; font="DejaVu Sans Mono", fontsize=11, halign=c == 1 ? :right : :center) + end + end + colgap!(gl, 10) + rowgap!(gl, 2) +end + +function add_threshold_curve_panel_3d!(fig, row, col, title, stats; outline_entries) + thresholds = [Float64(entry[:threshold_ratio]) for entry in stats] + f1 = [Float64(get(entry, :source_f1, entry[:f1])) for entry in stats] + precision = [Float64(entry[:precision]) for entry in stats] + recall = [Float64(get(entry, :source_recall, entry[:recall])) for entry in stats] + ax = Axis(fig[row, col]; title=title, xlabel="Threshold / max intensity", ylabel="Score") + lines!(ax, thresholds, f1; color=:cyan, linewidth=2.5, label="source F1") + lines!(ax, thresholds, precision; color=:magenta, linewidth=2.0, label="precision") + lines!(ax, thresholds, recall; color=:lime, linewidth=2.0, label="source recall") + for outline in outline_entries + threshold = Float64(outline.entry[:threshold_ratio]) + lines!(ax, [threshold, threshold], [0.0, 1.0]; color=(outline.color, 0.45), linewidth=1.5, linestyle=:dash) + end + ylims!(ax, 0, 1) + axislegend(ax; position=:rb, framevisible=false) + return nothing +end + +function save_threshold_boundary_detection_3d(path, pam_geo, pam_hasa, grid, cfg, sources; threshold_ratios, truth_radius, c=nothing) + truth_mask = pam_truth_mask_3d(sources, grid, cfg; radius=truth_radius) + geo_stats = threshold_detection_stats_3d(pam_geo, grid, cfg, sources; threshold_ratios=threshold_ratios, truth_radius=truth_radius, truth_mask=truth_mask) + hasa_stats = threshold_detection_stats_3d(pam_hasa, grid, cfg, sources; threshold_ratios=threshold_ratios, truth_radius=truth_radius, truth_mask=truth_mask) + global_ref = max(maximum(Float64.(pam_geo)), maximum(Float64.(pam_hasa)), eps(Float64)) + best_geo = best_threshold_entry_3d(geo_stats) + best_hasa = best_threshold_entry_3d(hasa_stats) + geo_outlines = threshold_outline_entries_3d(geo_stats) + hasa_outlines = threshold_outline_entries_3d(hasa_stats) + + fig = Figure(size=(1550, 1450)) + projections = (:depth_y, :depth_z, :y_z) + titles = Dict( + :depth_y => "Depth-Y max projection", + :depth_z => "Depth-Z max projection", + :y_z => "Y-Z max projection", + ) + hm = nothing + for (col, projection) in pairs(projections) + hm = add_projection_panel_3d!( + fig, 1, col, "Geometric: $(titles[projection])", + pam_geo, truth_mask, grid, cfg, sources; + projection=projection, + outline_entries=geo_outlines, + global_ref=global_ref, + c=c, + ) + add_projection_panel_3d!( + fig, 2, col, "HASA: $(titles[projection])", + pam_hasa, truth_mask, grid, cfg, sources; + projection=projection, + outline_entries=hasa_outlines, + global_ref=global_ref, + c=c, + ) + end + Colorbar(fig[1:2, 4], hm; label="Norm. PAM intensity") + legend_specs = [ + (label="best F1", color=:cyan), + (label="more recall", color=:lime), + (label="more precision", color=:magenta), + ] + legend_elements = [LineElement(color=spec.color, linewidth=3) for spec in legend_specs] + legend_labels = [spec.label for spec in legend_specs] + Legend(fig[3, 1:2], legend_elements, legend_labels; orientation=:horizontal, tellheight=true, framevisible=false) + Label(fig[3, 3], "Truth mask shown as dashed white contours; sources are x markers. Curves use the dense threshold search grid."; tellwidth=false, halign=:left) + add_threshold_curve_panel_3d!(fig, 4, 1:2, "Geometric threshold response", geo_stats; outline_entries=geo_outlines) + add_threshold_curve_panel_3d!(fig, 4, 3:4, "HASA threshold response", hasa_stats; outline_entries=hasa_outlines) + add_threshold_table_3d!(fig, 5, 1:2, "Geometric selected thresholds", geo_stats; outline_entries=geo_outlines) + add_threshold_table_3d!(fig, 5, 3:4, "HASA selected thresholds", hasa_stats; outline_entries=hasa_outlines) + save(path, fig) + return Dict( + "threshold_ratios" => threshold_ratios, + "selection_metric" => "source_f1", + "source_detection_radius_m" => Float64(truth_radius), + "best_geometric_threshold" => best_geo[:threshold_ratio], + "best_geometric_metric" => string_key_dict(best_geo), + "best_hasa_threshold" => best_hasa[:threshold_ratio], + "best_hasa_metric" => string_key_dict(best_hasa), + "geometric_selected_outlines" => [ + Dict("kind" => String(outline.kind), "label" => outline.label, "metric" => string_key_dict(outline.entry)) + for outline in geo_outlines + ], + "hasa_selected_outlines" => [ + Dict("kind" => String(outline.kind), "label" => outline.label, "metric" => string_key_dict(outline.entry)) + for outline in hasa_outlines + ], + "geometric" => [string_key_dict(stats) for stats in geo_stats], + "hasa" => [string_key_dict(stats) for stats in hasa_stats], + ) +end + +function _voxel_points_3d(mask::AbstractArray{Bool, 3}, grid, cfg::PAMConfig3D) + depth_mm = depth_coordinates_3d(cfg) .* 1e3 + y_mm = collect(grid.y) .* 1e3 + z_mm = collect(grid.z) .* 1e3 + idxs = Tuple.(findall(mask)) + return ( + depth = [depth_mm[idx[1]] for idx in idxs], + y = [y_mm[idx[2]] for idx in idxs], + z = [z_mm[idx[3]] for idx in idxs], + indices = idxs, + ) +end + +function save_best_threshold_volume_3d(path, intensity, grid, cfg, sources; threshold::Real, truth_radius::Real) + local_ref = max(maximum(Float64.(intensity)), eps(Float64)) + pred_mask = intensity .>= Float64(threshold) * local_ref + truth_mask = pam_truth_mask_3d(sources, grid, cfg; radius=truth_radius) + pred = _voxel_points_3d(pred_mask, grid, cfg) + truth = _voxel_points_3d(truth_mask, grid, cfg) + + fig = Figure(size=(1100, 900)) + ax = Axis3( + fig[1, 1]; + title="HASA 3D reconstructed region at best threshold $(round(Float64(threshold); digits=3))", + xlabel="Y [mm]", + ylabel="Z [mm]", + zlabel="Depth [mm]", + aspect=:data, + azimuth=0.75pi, + elevation=0.22pi, + ) + + if !isempty(truth.indices) + scatter!( + ax, + truth.y, + truth.z, + truth.depth; + markersize=10, + color=(:white, 0.16), + strokecolor=(:black, 0.25), + strokewidth=0.4, + ) + end + + if !isempty(pred.indices) + pred_values = [Float64(intensity[idx...]) / local_ref for idx in pred.indices] + sc = scatter!( + ax, + pred.y, + pred.z, + pred.depth; + markersize=14, + color=pred_values, + colormap=:viridis, + colorrange=(Float64(threshold), 1.0), + strokewidth=0, + ) + Colorbar(fig[1, 2], sc; label="Norm. HASA intensity") + else + Label(fig[1, 2], "No voxels at threshold."; tellheight=false) + end + + truth_sources = source_triples_mm(sources) + scatter!( + ax, + [t[2] for t in truth_sources], + [t[3] for t in truth_sources], + [t[1] for t in truth_sources]; + marker=:xcross, + markersize=24, + color=:red, + strokewidth=2, + ) + + Label( + fig[2, 1:2], + "Colored voxels are the thresholded HASA reconstruction; translucent white voxels are the truth mask; red x markers are source locations."; + tellwidth=false, + halign=:left, + ) + save(path, fig) + return Dict( + "threshold_ratio" => Float64(threshold), + "predicted_voxels" => count(pred_mask), + "truth_voxels" => count(truth_mask), + ) +end + +function save_napari_npz_3d(out_dir, pam_geo, pam_hasa, c, rho, grid, cfg, sources; truth_radius) + np = PythonCall.pyimport("numpy") + + depth_mm = Float32.(depth_coordinates_3d(cfg) .* 1e3) + y_mm = Float32.(collect(grid.y) .* 1e3) + z_mm = Float32.(collect(grid.z) .* 1e3) + + ref = max(maximum(Float64.(pam_hasa)), eps(Float64)) + hasa_norm = Float32.(Float64.(pam_hasa) ./ ref) + geo_norm = Float32.(Float64.(pam_geo) ./ ref) + c_vol = Float32.(c) + rho_vol = Float32.(rho) + truth_mask = Float32.(pam_truth_mask_3d(sources, grid, cfg; radius=truth_radius)) + + triples = source_triples_mm(sources) + src_depth = Float32[t[1] for t in triples] + src_y = Float32[t[2] for t in triples] + src_z = Float32[t[3] for t in triples] + + # voxel spacing in mm for napari scale parameter: (depth, y, z) + scale = [Float64(cfg.dx * 1e3), Float64(cfg.dy * 1e3), Float64(cfg.dz * 1e3)] + + npz_path = joinpath(out_dir, "napari_data.npz") + np.savez( + npz_path, + hasa = hasa_norm, + geometric = geo_norm, + sound_speed = c_vol, + density = rho_vol, + truth_mask = truth_mask, + depth_mm = depth_mm, + y_mm = y_mm, + z_mm = z_mm, + src_depth_mm = src_depth, + src_y_mm = src_y, + src_z_mm = src_z, + scale = Float64.(scale), + ) + + py_script = """ +import numpy as np, napari, sys + +data = np.load(r\"$(replace(npz_path, "\\" => "\\\\"))\") +scale = tuple(data[\"scale\"]) # (depth_mm, y_mm, z_mm) + +viewer = napari.Viewer(title=\"PAM 3D: $(basename(out_dir))\") +viewer.add_image(data[\"hasa\"], name=\"HASA (norm)\", scale=scale, colormap=\"inferno\", opacity=0.9) +viewer.add_image(data[\"geometric\"], name=\"Geometric (norm)\", scale=scale, colormap=\"viridis\", opacity=0.5, visible=False) +viewer.add_image(data[\"sound_speed\"], name=\"Sound speed [m/s]\", scale=scale, colormap=\"gray\", opacity=0.35, visible=False) +viewer.add_image(data[\"density\"], name=\"Density [kg/m3]\", scale=scale, colormap=\"gray\", opacity=0.35, visible=False) +viewer.add_image(data[\"truth_mask\"], name=\"Truth mask\", scale=scale, colormap=\"green\", opacity=0.25) + +depth_idx = np.interp(data[\"src_depth_mm\"], data[\"depth_mm\"], np.arange(len(data[\"depth_mm\"]))) +y_idx = np.interp(data[\"src_y_mm\"], data[\"y_mm\"], np.arange(len(data[\"y_mm\"]))) +z_idx = np.interp(data[\"src_z_mm\"], data[\"z_mm\"], np.arange(len(data[\"z_mm\"]))) +pts = np.stack([depth_idx, y_idx, z_idx], axis=1) +viewer.add_points(pts, name=\"Sources\", size=1.5, face_color=\"red\", symbol=\"cross\", scale=scale) + +napari.run() +""" + open(joinpath(out_dir, "view_pam.py"), "w") do io + write(io, py_script) + end + + println("Saved napari data -> $npz_path") + println(" Open with: python $(joinpath(out_dir, "view_pam.py"))") +end diff --git a/src/pam/reconstruction3d.jl b/src/pam/3d/reconstruction3d.jl similarity index 82% rename from src/pam/reconstruction3d.jl rename to src/pam/3d/reconstruction3d.jl index 08771b8..3d74803 100644 --- a/src/pam/reconstruction3d.jl +++ b/src/pam/3d/reconstruction3d.jl @@ -435,6 +435,142 @@ function _reconstruct_pam_cuda_3d( return raws, timing end +function _reconstruct_pam_cpu_3d( + c_padded::AbstractArray{<:Real, 3}, + rf::AbstractArray{<:Real, 3}, + cfg::PAMConfig3D, + selected_freqs::AbstractVector{<:Real}, + selected_bins::AbstractVector{<:Integer}, + crop_range_y::UnitRange{Int}, + crop_range_z::UnitRange{Int}, + padded_ny::Int, + padded_nz::Int, + rr::Int, + row_stop::Int, + c0::Float64, + effective_axial_step::Float64, + axial_substeps::Int, + corrected::Bool, + t0::Float64, + recon_label::AbstractString, + show_progress::Bool, +) + t_setup_start = time() + nx = size(c_padded, 1) + crop_ny = length(crop_range_y) + crop_nz = length(crop_range_z) + crop_y0 = first(crop_range_y) + crop_z0 = first(crop_range_z) + nfreq = length(selected_freqs) + + # Sound-speed contrast field: (padded_ny, padded_nz, nx), matching GPU eta layout + eta_spatial = permutedims(Float64.(1.0 .- (c0 ./ c_padded) .^ 2), (2, 3, 1)) + + k_y = _fft_wavenumbers(padded_ny, cfg.dy) + k_z = _fft_wavenumbers(padded_nz, cfg.dz) + k_lat2 = reshape(k_y, :, 1) .^ 2 .+ reshape(k_z, 1, :) .^ 2 + k_radii = sqrt.(k_lat2) + + # Pre-plan FFTW once for the padded lateral plane; MEASURE finds the optimal kernel + dummy = zeros(ComplexF64, padded_ny, padded_nz) + plan_fwd = plan_fft(dummy, (1, 2); flags=FFTW.MEASURE) + plan_bwd = plan_ifft(dummy, (1, 2); flags=FFTW.MEASURE) + + rf_fft = fft(Float64.(rf), 3) # time FFT: (ny, nz, nt) + t_setup_s = time() - t_setup_start + + intensity = zeros(Float64, nx, crop_ny, crop_nz) + + println("[ PAM 3D ] $recon_label: CPU (single-threaded FFTW, MEASURE), $nfreq freq bins") + flush(stdout) + + march_wall_start = time() + + for (freq, bin) in zip(selected_freqs, selected_bins) + k0 = 2π * freq / c0 + k0_sq = k0^2 + + k_axial = sqrt.(complex.(k0^2 .- k_lat2, 0.0)) + propagating = real.(k_axial ./ k0) .> 0.0 + propagator = exp.(1im .* k_axial .* effective_axial_step) .* propagating + + k_max_prop = maximum(k_radii[propagating]; init=0.0) + weighting = _tukey_radial(k_radii, k_max_prop, cfg.tukey_ratio) .* propagating + + correction = zeros(ComplexF64, padded_ny, padded_nz) + if corrected + for j in eachindex(k_axial) + propagating[j] || continue + abs(k_axial[j]) > sqrt(eps(Float64)) || continue + correction[j] = propagator[j] * effective_axial_step / (2im * k_axial[j]) + end + end + + # Apply ifftshift to match GPU FFT convention + prop_shifted = _ifftshift_2d(propagating .* propagator) + weight_shifted = _ifftshift_2d(weighting) + prop_n_weight = _ifftshift_2d(propagating .* propagator .^ axial_substeps .* weighting) + prop_w = corrected ? prop_shifted .* weight_shifted : nothing + corr_w = corrected ? _ifftshift_2d(propagating .* correction) .* weight_shifted : nothing + + # Initial condition: place RF frequency slice into padded array + phase = cis(-2π * freq * t0) + p0 = zeros(ComplexF64, padded_ny, padded_nz) + p0[crop_range_y, crop_range_z] .= rf_fft[:, :, bin] .* phase + current = plan_fwd * p0 + current .*= weight_shifted + + for row in (rr + 1):row_stop + if corrected + eta_slice = @view eta_spatial[:, :, row] + for _ in 1:(axial_substeps - 1) + p_spatial = plan_bwd * current + lp_fft = plan_fwd * (k0_sq .* eta_slice .* p_spatial) + current = current .* prop_w .+ corr_w .* lp_fft + end + p_spatial = plan_bwd * current + lp_fft = plan_fwd * (k0_sq .* eta_slice .* p_spatial) + current = current .* prop_w .+ corr_w .* lp_fft + else + current .*= prop_n_weight + end + p_row = plan_bwd * current + for iz in 1:crop_nz, iy in 1:crop_ny + v = p_row[crop_y0 + iy - 1, crop_z0 + iz - 1] + intensity[row, iy, iz] += real(v)^2 + imag(v)^2 + end + end + end + + march_wall_s = time() - march_wall_start + + _pam_progress( + show_progress, + "PAM 3D $recon_label CPU: march $(round(march_wall_s; digits=1)) s, $nfreq freq bins", + ) + + timing = Dict{Symbol, Any}( + :setup_s => t_setup_s, + :operator_setup_s => t_setup_s, + :batch_setup_s => 0.0, + :march_wall_s => march_wall_s, + :march_cpu_s => march_wall_s, + :march_gpu_s => nothing, + :download_s => 0.0, + :bandwidth_GBps => nothing, + :fft_s => nothing, + :elementwise_s => nothing, + :nrows => row_stop - rr, + :nfreq => nfreq, + :nwindows => 1, + :padded_ny => padded_ny, + :padded_nz => padded_nz, + :axial_substeps => axial_substeps, + :bytes_march_est => nothing, + ) + return [intensity], timing +end + function reconstruct_pam_3d( rf::AbstractArray{<:Real, 3}, c::AbstractArray{<:Real, 3}, @@ -455,7 +591,6 @@ function reconstruct_pam_3d( nx, ny, nz = size(c) size(rf, 1) == ny && size(rf, 2) == nz || error("RF data must have size (ny, nz, nt); expected ($ny, $nz, ·), got ($(size(rf,1)), $(size(rf,2)), ·).") - nt = size(rf, 3) rr = receiver_row(cfg) rr <= nx || error("Receiver row lies outside the computational grid.") @@ -482,17 +617,26 @@ function reconstruct_pam_3d( "grid=$(nx)×$(ny)×$(nz), padded=($(padded_ny)×$(padded_nz)), substeps=$axial_substeps", ) - use_gpu || error("CPU path not implemented for 3D PAM; use use_gpu=true.") - - setup = _pam_cuda_setup_3d( - c_padded, cfg, selected_freqs, selected_bins, - crop_range_y, crop_range_z, - nx, padded_ny, padded_nz, rr, row_stop, - c0, effective_axial_step, axial_substeps, - ) - raws, gpu_timing = _reconstruct_pam_cuda_3d( - setup, [rf], [t0], corrected, recon_label, show_progress, benchmark, - ) + raws, gpu_timing = if use_gpu + setup = _pam_cuda_setup_3d( + c_padded, cfg, selected_freqs, selected_bins, + crop_range_y, crop_range_z, + nx, padded_ny, padded_nz, rr, row_stop, + c0, effective_axial_step, axial_substeps, + ) + _reconstruct_pam_cuda_3d( + setup, [rf], [t0], corrected, recon_label, show_progress, benchmark, + ) + else + _reconstruct_pam_cpu_3d( + c_padded, rf, cfg, + selected_freqs, selected_bins, + crop_range_y, crop_range_z, + padded_ny, padded_nz, rr, row_stop, + c0, effective_axial_step, axial_substeps, + corrected, t0, recon_label, show_progress, + ) + end intensity = raws[1] # (nx, ny, nz) _apply_axial_gain_3d!(intensity, cfg) @@ -510,8 +654,8 @@ function reconstruct_pam_3d( :axial_substeps_per_cell => axial_substeps, :time_origin => t0, :use_gpu => use_gpu, - :backend => :cuda, - :gpu_precision => _PAM_CUDA_PRECISION, + :backend => use_gpu ? :cuda : :cpu, + :gpu_precision => use_gpu ? _PAM_CUDA_PRECISION : nothing, :axial_gain_power => cfg.axial_gain_power, :show_progress => show_progress, :benchmark => benchmark, diff --git a/src/pam/sources3d.jl b/src/pam/3d/sources3d.jl similarity index 97% rename from src/pam/sources3d.jl rename to src/pam/3d/sources3d.jl index a48c673..238c61c 100644 --- a/src/pam/sources3d.jl +++ b/src/pam/3d/sources3d.jl @@ -17,7 +17,6 @@ Base.@kwdef struct BubbleCluster3D <: EmissionSource3D lateral_z::Float64 = 0.0 fundamental::Float64 = 5e5 amplitude::Float64 = 1.0 - n_bubbles::Float64 = 1.0 harmonics::Vector{Int} = [2, 3, 4] harmonic_amplitudes::Vector{Float64} = [1.0, 0.6, 0.3] harmonic_phases::Vector{Float64} = [0.0, 0.0, 0.0] @@ -54,13 +53,12 @@ function _source_signal(nt::Int, dt::Real, src::BubbleCluster3D) active = findall((t .>= 0.0) .& (t .<= src.gate_duration)) isempty(active) && return signal envelope = _tukey_window(length(active), src.taper_ratio) - total_amp = src.amplitude * src.n_bubbles t_active = t[active] accumulator = zeros(Float64, length(active)) @inbounds for i in eachindex(src.harmonics) accumulator .+= src.harmonic_amplitudes[i] .* cos.(2pi .* src.harmonics[i] .* src.fundamental .* t_active .+ src.harmonic_phases[i]) end - signal[active] .= total_amp .* envelope .* accumulator + signal[active] .= src.amplitude .* envelope .* accumulator return signal end @@ -263,7 +261,7 @@ function _resample_source_phases_3d( BubbleCluster3D( depth=src.depth, lateral_y=src.lateral_y, lateral_z=src.lateral_z, fundamental=src.fundamental, amplitude=src.amplitude, - n_bubbles=src.n_bubbles, harmonics=copy(src.harmonics), + harmonics=copy(src.harmonics), harmonic_amplitudes=copy(src.harmonic_amplitudes), harmonic_phases=2pi .* rand(rng, length(src.harmonics)), gate_duration=src.gate_duration, taper_ratio=src.taper_ratio, @@ -306,7 +304,7 @@ function _expand_sources_per_window( BubbleCluster3D( depth=src.depth, lateral_y=src.lateral_y, lateral_z=src.lateral_z, fundamental=src.fundamental * fscale, amplitude=src.amplitude, - n_bubbles=src.n_bubbles, harmonics=copy(src.harmonics), + harmonics=copy(src.harmonics), harmonic_amplitudes=copy(src.harmonic_amplitudes), harmonic_phases=2pi .* rand(rng, length(src.harmonics)), gate_duration=min(src.gate_duration, frame_dur), taper_ratio=src.taper_ratio, @@ -358,7 +356,6 @@ function make_squiggle_bubble_sources_3d( lateral_z_bounds::Tuple{<:Real, <:Real} = (-Inf, Inf), fundamental::Real = 5e5, amplitude::Real = 1.0, - n_bubbles::Real = 1.0, harmonics::AbstractVector{<:Integer} = [2, 3, 4], harmonic_amplitudes::AbstractVector{<:Real} = [1.0, 0.6, 0.3], gate_duration::Real = 50e-6, @@ -425,7 +422,6 @@ function make_squiggle_bubble_sources_3d( lateral_z = z, fundamental = Float64(fundamental), amplitude = Float64(amplitude), - n_bubbles = Float64(n_bubbles), harmonics = copy(harmonics_i), harmonic_amplitudes = copy(harmonic_amplitudes_f), harmonic_phases = phases, @@ -718,7 +714,6 @@ volume edge. """ function make_network_bubble_sources_3d( centers::AbstractVector; - sphere_radius::Real = 0.0, axial_radius::Real = 10e-3, lateral_y_radius::Real = 1.5e-3, lateral_z_radius::Real = 1.5e-3, @@ -741,7 +736,6 @@ function make_network_bubble_sources_3d( lateral_z_bounds::Tuple{<:Real, <:Real} = (-Inf, Inf), fundamental::Real = 5e5, amplitude::Real = 1.0, - n_bubbles::Real = 1.0, harmonics::AbstractVector{<:Integer} = [2, 3, 4], harmonic_amplitudes::AbstractVector{<:Real} = [1.0, 0.6, 0.3], gate_duration::Real = 50e-6, @@ -762,10 +756,7 @@ function make_network_bubble_sources_3d( length(harmonic_amplitudes_f) == length(harmonics_i) || error("harmonic_amplitudes must have the same length as harmonics.") mode = _normalize_cluster_phase_mode(phase_mode) - legacy_radius = Float64(sphere_radius) - ellipsoid_radii = legacy_radius > 0 ? - (legacy_radius, legacy_radius, legacy_radius) : - (Float64(axial_radius), Float64(lateral_y_radius), Float64(lateral_z_radius)) + ellipsoid_radii = (Float64(axial_radius), Float64(lateral_y_radius), Float64(lateral_z_radius)) all(r -> r > 0, ellipsoid_radii) || error("network ellipsoid radii must be positive.") legacy_density_sigma = Float64(density_sigma) density_sigmas = legacy_density_sigma > 0 ? @@ -820,7 +811,6 @@ function make_network_bubble_sources_3d( lateral_z=z, fundamental=Float64(fundamental), amplitude=Float64(amplitude), - n_bubbles=Float64(n_bubbles), harmonics=copy(harmonics_i), harmonic_amplitudes=copy(harmonic_amplitudes_f), harmonic_phases=phases, @@ -842,7 +832,6 @@ function make_network_bubble_sources_3d( meta = Dict{Symbol, Any}( :source_model => :network, :centers => center_triples, - :sphere_radius => legacy_radius > 0 ? legacy_radius : nothing, :ellipsoid_radii => ellipsoid_radii, :axial_radius => ellipsoid_radii[1], :lateral_y_radius => ellipsoid_radii[2], diff --git a/src/pam/3d/workflow3d.jl b/src/pam/3d/workflow3d.jl new file mode 100644 index 0000000..20200f6 --- /dev/null +++ b/src/pam/3d/workflow3d.jl @@ -0,0 +1,111 @@ +function run_pam_case_3d( + c::AbstractArray{<:Real, 3}, + rho::AbstractArray{<:Real, 3}, + sources::AbstractVector{<:EmissionSource3D}, + cfg::PAMConfig3D; + frequencies::Union{Nothing, AbstractVector{<:Real}}=nothing, + bandwidth::Real=0.0, + kwave_use_gpu::Bool=true, + recon_use_gpu::Bool=true, + reconstruction_axial_step::Union{Nothing, Real}=nothing, + reconstruction_mode::Symbol=:full, + window_config::PAMWindowConfig=PAMWindowConfig(), + show_progress::Bool=false, + benchmark::Bool=false, + window_batch::Int=1, + simulation_backend::Symbol=:analytic, + source_phase_mode::Symbol=:coherent, + rng::Random.AbstractRNG=Random.default_rng(), + source_variability::SourceVariabilityConfig=SourceVariabilityConfig(), +) + recon_use_gpu || error("3D PAM reconstruction currently requires --recon-use-gpu=true.") + recon_freqs = isnothing(frequencies) ? default_recon_frequencies(sources) : Float64.(frequencies) + phase_mode = _normalize_source_phase_mode(source_phase_mode) + recon_mode = phase_mode == :random_phase_per_window ? + :windowed : + _normalize_reconstruction_mode(reconstruction_mode) + effective_window_config = PAMWindowConfig(; + enabled=recon_mode == :windowed, + window_duration=window_config.window_duration, + hop=window_config.hop, + taper=window_config.taper, + min_energy_ratio=window_config.min_energy_ratio, + accumulation=window_config.accumulation, + ) + sim_sources = sources + n_frames = 1 + if phase_mode == :random_static_phase + sim_sources = _resample_source_phases_3d(sources, rng) + elseif phase_mode == :random_phase_per_window + sim_sources, n_frames = _expand_sources_per_window( + sources, + effective_window_config.window_duration, + effective_window_config.hop, + cfg.t_max, + rng; + variability=source_variability, + ) + end + rf, grid, sim_info = if simulation_backend == :kwave + simulate_point_sources_3d(c, rho, sim_sources, cfg; use_gpu=kwave_use_gpu) + else + analytic_rf_for_point_sources_3d(cfg, sim_sources) + end + recon_kwargs = ( + frequencies=recon_freqs, + bandwidth=bandwidth, + reference_sound_speed=_pam_reference_sound_speed(c, cfg, sources), + axial_step=reconstruction_axial_step, + use_gpu=recon_use_gpu, + show_progress=show_progress, + benchmark=benchmark, + window_batch=window_batch, + ) + pam_geo, _, geo_info = if recon_mode == :windowed + reconstruct_pam_windowed_3d( + rf, + c, + cfg; + recon_kwargs..., + corrected=false, + window_config=effective_window_config, + ) + else + reconstruct_pam_3d(rf, c, cfg; recon_kwargs..., corrected=false) + end + pam_hasa, _, hasa_info = if recon_mode == :windowed + reconstruct_pam_windowed_3d( + rf, + c, + cfg; + recon_kwargs..., + corrected=true, + window_config=effective_window_config, + ) + else + reconstruct_pam_3d(rf, c, cfg; recon_kwargs..., corrected=true) + end + + return Dict{Symbol, Any}( + :rf => Float64.(rf), + :kgrid => grid, + :simulation => sim_info, + :pam_geo => pam_geo, + :pam_hasa => pam_hasa, + :geo_info => geo_info, + :hasa_info => hasa_info, + :stats_geo => any(s -> s isa BubbleCluster3D, sources) ? Dict{Symbol,Any}() : analyse_pam_3d(pam_geo, grid, cfg, sources), + :stats_hasa => any(s -> s isa BubbleCluster3D, sources) ? Dict{Symbol,Any}() : analyse_pam_3d(pam_hasa, grid, cfg, sources), + :reconstruction_frequencies => recon_freqs, + :analysis_mode => any(s -> s isa BubbleCluster3D, sources) ? :detection : :localization, + :analysis_source_count => length(sources), + :emission_event_count => length(sim_sources), + :reconstruction_mode => recon_mode, + :source_phase_mode => phase_mode, + :n_frames => n_frames, + :window_config => _window_config_info(effective_window_config), + :kwave_use_gpu => kwave_use_gpu, + :recon_use_gpu => recon_use_gpu, + :show_progress => show_progress, + ) +end diff --git a/src/pam/analysis3d.jl b/src/pam/analysis3d.jl deleted file mode 100644 index 562de79..0000000 --- a/src/pam/analysis3d.jl +++ /dev/null @@ -1,149 +0,0 @@ -function find_pam_peaks_3d( - intensity::AbstractArray{<:Real, 3}, - ::NamedTuple, - cfg::PAMConfig3D; - n_peaks::Integer, - suppression_radius::Real=cfg.peak_suppression_radius, -) - work = copy(Float64.(intensity)) - row_start = receiver_row(cfg) + 1 - pml_guard = _pam_pml_guard_3d(cfg) - row_stop = max(row_start, size(work, 1) - pml_guard) - row_start <= row_stop || error("No valid reconstruction rows remain after excluding the receiver row and PML.") - work[1:(row_start - 1), :, :] .= -Inf - if row_stop < size(work, 1) - work[(row_stop + 1):end, :, :] .= -Inf - end - # Suppress the outermost lateral cell on each edge (aperture boundary fold-back). - work[:, 1, :] .= -Inf - work[:, end, :] .= -Inf - work[:, :, 1] .= -Inf - work[:, :, end] .= -Inf - - rad_rows = max(1, round(Int, suppression_radius / cfg.dx)) - rad_y = max(1, round(Int, suppression_radius / cfg.dy)) - rad_z = max(1, round(Int, suppression_radius / cfg.dz)) - peaks = NTuple{3, Int}[] - - for _ in 1:Int(n_peaks) - idx = Tuple(argmax(work)) - isfinite(work[idx...]) || break - push!(peaks, idx) - r0, y0, z0 = idx - work[max(1, r0 - rad_rows):min(size(work, 1), r0 + rad_rows), - max(1, y0 - rad_y):min(size(work, 2), y0 + rad_y), - max(1, z0 - rad_z):min(size(work, 3), z0 + rad_z)] .= -Inf - end - - return peaks -end - -function pam_truth_mask_3d( - sources::AbstractVector{<:EmissionSource3D}, - grid::NamedTuple, - cfg::PAMConfig3D; - radius::Real=cfg.success_tolerance, -) - radius_m = Float64(radius) - radius_m >= 0 || error("truth-mask radius must be non-negative.") - nx, ny, nz = pam_Nx(cfg), pam_Ny(cfg), pam_Nz(cfg) - mask = falses(nx, ny, nz) - x = collect(grid.x) - y = collect(grid.y) - z = collect(grid.z) - r0 = receiver_row(cfg) - radius2 = radius_m^2 - row_r = ceil(Int, radius_m / cfg.dx) - col_r_y = ceil(Int, radius_m / cfg.dy) - col_r_z = ceil(Int, radius_m / cfg.dz) - - for src in sources - depth = src.depth - src_x = x[r0] + depth - row0 = r0 + round(Int, depth / cfg.dx) - col0_y = argmin(abs.(y .- src.lateral_y)) - col0_z = argmin(abs.(z .- src.lateral_z)) - for row in max(r0 + 1, row0 - row_r):min(nx, row0 + row_r) - dx2 = (x[row] - src_x)^2 - for iy in max(1, col0_y - col_r_y):min(ny, col0_y + col_r_y) - dy2 = (y[iy] - src.lateral_y)^2 - for iz in max(1, col0_z - col_r_z):min(nz, col0_z + col_r_z) - dz2 = (z[iz] - src.lateral_z)^2 - if dx2 + dy2 + dz2 <= radius2 - mask[row, iy, iz] = true - end - end - end - end - end - return mask -end - -function analyse_pam_3d( - intensity::AbstractArray{<:Real, 3}, - grid::NamedTuple, - cfg::PAMConfig3D, - sources::AbstractVector{<:EmissionSource3D}; - n_peaks::Union{Nothing, Integer}=nothing, - success_tolerance::Real=cfg.success_tolerance, - suppression_radius::Real=cfg.peak_suppression_radius, -) - n_truth = length(sources) - n_truth > 0 || error("At least one emission source is required for 3D PAM analysis.") - n_find = isnothing(n_peaks) ? n_truth : Int(n_peaks) - peaks = find_pam_peaks_3d(intensity, grid, cfg; n_peaks=n_find, suppression_radius=suppression_radius) - length(peaks) == n_truth || error("Expected to recover $n_truth peaks, found $(length(peaks)).") - - x = collect(grid.x) - y = collect(grid.y) - z = collect(grid.z) - rr = receiver_row(cfg) - - truth_mm = [(src.depth * 1e3, src.lateral_y * 1e3, src.lateral_z * 1e3) for src in sources] - pred_mm = [((x[idx[1]] - x[rr]) * 1e3, y[idx[2]] * 1e3, z[idx[3]] * 1e3) for idx in peaks] - - cost = Matrix{Float64}(undef, n_truth, n_truth) - for i in 1:n_truth, j in 1:n_truth - d_ax = truth_mm[i][1] - pred_mm[j][1] - d_lat_y = truth_mm[i][2] - pred_mm[j][2] - d_lat_z = truth_mm[i][3] - pred_mm[j][3] - cost[i, j] = sqrt(d_ax^2 + d_lat_y^2 + d_lat_z^2) - end - assignment, _ = _best_assignment(cost) - - matched_pred_mm = [pred_mm[assignment[i]] for i in 1:n_truth] - matched_indices = [peaks[assignment[i]] for i in 1:n_truth] - axial_errors_mm = [truth_mm[i][1] - matched_pred_mm[i][1] for i in 1:n_truth] - lateral_y_errors_mm = [truth_mm[i][2] - matched_pred_mm[i][2] for i in 1:n_truth] - lateral_z_errors_mm = [truth_mm[i][3] - matched_pred_mm[i][3] for i in 1:n_truth] - radial_errors_mm = [sqrt(axial_errors_mm[i]^2 + lateral_y_errors_mm[i]^2 + lateral_z_errors_mm[i]^2) for i in 1:n_truth] - - raw_peak_intensities = [Float64(intensity[idx...]) for idx in matched_indices] - max_intensity = max(maximum(Float64.(intensity)), eps(Float64)) - norm_peak_intensities = raw_peak_intensities ./ max_intensity - - tol_mm = Float64(success_tolerance) * 1e3 - successes = radial_errors_mm .<= tol_mm - num_success = count(identity, successes) - - return Dict{Symbol, Any}( - :truth_mm => truth_mm, - :predicted_mm => matched_pred_mm, - :peak_indices => matched_indices, - :axial_errors_mm => axial_errors_mm, - :lateral_y_errors_mm => lateral_y_errors_mm, - :lateral_z_errors_mm => lateral_z_errors_mm, - :radial_errors_mm => radial_errors_mm, - :mean_axial_error_mm => mean(abs.(axial_errors_mm)), - :mean_lateral_y_error_mm => mean(abs.(lateral_y_errors_mm)), - :mean_lateral_z_error_mm => mean(abs.(lateral_z_errors_mm)), - :mean_radial_error_mm => mean(radial_errors_mm), - :max_radial_error_mm => maximum(radial_errors_mm), - :success_tolerance_mm => tol_mm, - :success_rate => num_success / n_truth, - :num_success => num_success, - :raw_peak_intensities => raw_peak_intensities, - :norm_peak_intensities => norm_peak_intensities, - :mean_norm_peak_intensity => mean(norm_peak_intensities), - ) -end diff --git a/src/pam/setup/config.jl b/src/pam/setup/config.jl new file mode 100644 index 0000000..15b9888 --- /dev/null +++ b/src/pam/setup/config.jl @@ -0,0 +1,360 @@ +# CLI/config parsing helpers for PAM runner scripts. + +Base.@kwdef struct CLIOption + name::String + default::String + value::String = "string" + category::String = "General" + applies_to::String = "PAM" + choices::Vector{String} = String[] + description::String = "" +end + +function _cli_option(name, default, value, category, applies_to, description; choices=String[]) + return CLIOption( + name=name, + default=default, + value=value, + category=category, + applies_to=applies_to, + choices=choices, + description=description, + ) +end + +function pam_cli_options() + return CLIOption[ + _cli_option("dimension", "2", "2|3", "General", "PAM", "Selects the 2D or 3D PAM workflow."; choices=["2", "3"]), + _cli_option("source-model", "squiggle", "point|squiggle|network", "General", "PAM", "Selects explicit point sources, a squiggly vascular source, or a synthetic 3D network."; choices=["point", "squiggle", "network"]), + _cli_option("from-run-dir", "", "path", "General", "2D reconstruction only", "Loads RF data, medium, grid, and sources from a previous output directory and reruns reconstruction/analysis only."), + _cli_option("random-seed", "42", "integer", "General", "PAM", "Seed used for stochastic phases, source placement jitter, and generated vascular/network geometry."), + _cli_option("benchmark", "false", "bool", "General", "PAM", "Prints additional timing information from simulation and reconstruction."), + + _cli_option("sources-mm", "30:0", "depth:lateral[,depth:lateral] or depth:y:z", "Source geometry", "point", "Point source coordinates in millimeters. 2D uses depth:lateral; 3D uses depth:y:z."), + _cli_option("anchors-mm", "45:0", "depth:lateral[,depth:lateral] or depth:y:z", "Source geometry", "squiggle, network", "Anchor coordinates for generated vascular or network activity. 2D uses depth:lateral; 3D uses depth:y:z."), + _cli_option("frequency-mhz", "0.4", "MHz", "Source signal", "point", "Tone-burst frequency for point sources unless per-source frequencies are supplied."), + _cli_option("fundamental-mhz", "0.5", "MHz", "Source signal", "squiggle, network", "Fundamental activity frequency. Harmonic frequencies are integer multiples of this value."), + _cli_option("amplitude-pa", "1.0", "pressure", "Source signal", "PAM", "Default pressure amplitude for generated sources."), + _cli_option("source-amplitudes-pa", "", "comma list", "Source signal", "point", "Optional per-point-source amplitudes. Use one value for all sources or one value per source."), + _cli_option("source-frequencies-mhz", "", "comma list", "Source signal", "point", "Optional per-point-source frequencies in MHz. Use one value for all sources or one value per source."), + _cli_option("phases-deg", "", "comma list", "Source signal", "point", "Optional per-point-source phases in degrees before phase-mode randomization."), + _cli_option("delays-us", "0", "comma list", "Source signal", "PAM", "Emission delays in microseconds. Use one value for all sources or one value per coordinate/anchor."), + _cli_option("num-cycles", "4", "integer", "Source signal", "point", "Number of cycles in each point-source tone burst."), + _cli_option("harmonics", "2,3,4", "comma list", "Source signal", "squiggle, network", "Harmonic orders emitted by generated bubble activity."), + _cli_option("harmonic-amplitudes", "1.0,0.6,0.3", "comma list", "Source signal", "squiggle, network", "Relative amplitude for each harmonic listed in --harmonics."), + _cli_option("gate-us", "50", "microseconds", "Source signal", "squiggle, network", "Duration of each activity emission gate."), + _cli_option("taper-ratio", "0.25", "fraction", "Source signal", "squiggle, network", "Tukey taper fraction applied to generated activity gates."), + _cli_option("phase-mode", "geometric", "coherent|random|jittered|geometric", "Source signal", "PAM", "Controls initial source phases. Point sources accept coherent, random, and jittered; generated activity also uses geometric travel-time phases."; choices=["coherent", "random", "jittered", "geometric"]), + _cli_option("phase-jitter-rad", "0.2", "radians", "Source signal", "PAM", "Standard deviation for jittered source phases."), + _cli_option("source-phase-mode", "random_phase_per_window", "coherent|random_static_phase|random_phase_per_window", "Source signal", "PAM", "Controls whether source phases are fixed or redrawn across reconstruction windows."; choices=["coherent", "random_static_phase", "random_phase_per_window"]), + _cli_option("frequency-jitter-percent", "1", "percent", "Source signal", "squiggle, network", "Multiplicative jitter applied to generated source fundamentals before harmonics are formed."), + _cli_option("transducer-mm", "-30:0", "depth:lateral", "Source geometry", "2D squiggle", "Reference transducer position used when computing geometric source phases in 2D."), + + _cli_option("vascular-length-mm", "12", "mm", "Vascular source", "squiggle", "Length of the generated squiggle centerline for each anchor."), + _cli_option("vascular-squiggle-amplitude-mm", "1.5", "mm", "Vascular source", "squiggle", "Lateral squiggle amplitude in 2D, or y-amplitude in 3D."), + _cli_option("vascular-squiggle-amplitude-x-mm", "1.0", "mm", "Vascular source", "3D squiggle", "Depth-direction squiggle amplitude for 3D vascular sources."), + _cli_option("vascular-squiggle-wavelength-mm", "8", "mm", "Vascular source", "squiggle", "Spatial wavelength of the generated squiggle path."), + _cli_option("vascular-squiggle-slope", "0.0", "slope", "Vascular source", "squiggle", "Linear slope added to the generated squiggle path."), + _cli_option("squiggle-phase-x-deg", "90", "degrees", "Vascular source", "3D squiggle", "Phase offset for the 3D depth-direction squiggle component."), + _cli_option("vascular-source-spacing-mm", "0.5", "mm", "Vascular source", "squiggle, network", "Approximate spacing between sampled bubble emitters along generated centerlines."), + _cli_option("vascular-position-jitter-mm", "0.05", "mm", "Vascular source", "squiggle", "Random position jitter applied when sampling vascular sources."), + _cli_option("vascular-min-separation-mm", "0.25", "mm", "Vascular source", "squiggle, network", "Minimum allowed distance between generated bubble emitters."), + _cli_option("vascular-max-sources-per-anchor", "0", "integer", "Vascular source", "squiggle", "Caps generated sources per anchor. A value of 0 disables the cap."), + _cli_option("vascular-radius-mm", "1.0", "mm", "Analysis", "squiggle, network", "Truth radius used when scoring activity detection around generated sources."), + + _cli_option("network-axial-radius-mm", "10.0", "mm", "Network source", "3D network", "Axial radius of the ellipsoid used to clip generated network activity."), + _cli_option("network-lateral-y-radius-mm", "1.5", "mm", "Network source", "3D network", "Y radius of the generated network ellipsoid."), + _cli_option("network-lateral-z-radius-mm", "1.5", "mm", "Network source", "3D network", "Z radius of the generated network ellipsoid."), + _cli_option("network-root-count", "12", "integer", "Network source", "3D network", "Number of root branches grown for each network center."), + _cli_option("network-generations", "3", "integer", "Network source", "3D network", "Number of branching generations in the synthetic network."), + _cli_option("network-branch-length-mm", "5.0", "mm", "Network source", "3D network", "Nominal length of each generated branch segment."), + _cli_option("network-branch-step-mm", "0.4", "mm", "Network source", "3D network", "Sampling step along generated network branches."), + _cli_option("network-branch-angle-deg", "36", "degrees", "Network source", "3D network", "Nominal branching angle for synthetic network growth."), + _cli_option("network-tortuosity", "0.18", "fraction", "Network source", "3D network", "Strength of random branch curvature in the synthetic network."), + _cli_option("network-orientation", "isotropic", "isotropic|horizontal|axial", "Network source", "3D network", "Orientation prior for generated network branches."; choices=["isotropic", "horizontal", "axial"]), + _cli_option("network-density-sigma-mm", "0", "mm", "Network source", "3D network", "Optional isotropic Gaussian density sigma. A value of 0 uses the anisotropic sigma options."), + _cli_option("network-density-axial-sigma-mm", "10.0", "mm", "Network source", "3D network", "Axial Gaussian density sigma for network source sampling."), + _cli_option("network-density-lateral-y-sigma-mm", "1.5", "mm", "Network source", "3D network", "Y Gaussian density sigma for network source sampling."), + _cli_option("network-density-lateral-z-sigma-mm", "1.5", "mm", "Network source", "3D network", "Z Gaussian density sigma for network source sampling."), + _cli_option("network-max-sources-per-center", "80", "integer", "Network source", "3D network", "Caps generated sources per network center. Values <= 0 disable the cap."), + + _cli_option("axial-mm", "80", "mm", "Grid", "PAM", "Requested axial domain depth. The runner may extend this to fit sources and time of flight."), + _cli_option("transverse-mm", "102.4", "mm", "Grid", "PAM", "Default lateral domain width. In 3D this seeds y and z widths unless overridden."), + _cli_option("transverse-y-mm", "", "mm", "Grid", "3D", "Overrides the 3D y-width when set."), + _cli_option("transverse-z-mm", "", "mm", "Grid", "3D", "Overrides the 3D z-width when set."), + _cli_option("dx-mm", "0.2", "mm", "Grid", "PAM", "Axial grid spacing."), + _cli_option("dy-mm", "", "mm", "Grid", "3D", "3D y grid spacing. Defaults to --dz-mm when omitted."), + _cli_option("dz-mm", "0.2", "mm", "Grid", "PAM", "2D lateral spacing or 3D z spacing."), + _cli_option("t-max-us", "500", "microseconds", "Grid", "PAM", "Requested simulation duration. The runner may extend this when needed to capture source arrivals."), + _cli_option("dt-ns", "20", "nanoseconds", "Grid", "PAM", "Simulation time step."), + _cli_option("zero-pad-factor", "4", "integer", "Grid", "PAM", "Lateral FFT zero-padding factor used by ASA/HASA reconstruction."), + _cli_option("bottom-margin-mm", "10", "mm", "Grid", "PAM", "Minimum margin below the deepest source when auto-fitting the PAM domain."), + _cli_option("receiver-aperture-mm", "full", "mm|full", "Receiver", "PAM", "Receiver aperture width. Use full, all, or none to use the whole receiver plane."), + _cli_option("receiver-aperture-y-mm", "", "mm|full", "Receiver", "3D", "Overrides the 3D receiver aperture in y."), + _cli_option("receiver-aperture-z-mm", "", "mm|full", "Receiver", "3D", "Overrides the 3D receiver aperture in z."), + _cli_option("peak-suppression-radius-mm", "8.0", "mm", "Analysis", "PAM", "Radius used to suppress neighboring peaks during localization analysis."), + _cli_option("success-tolerance-mm", "1.5", "mm", "Analysis", "PAM", "Localization error threshold used when reporting success."), + _cli_option("axial-gain-power", "1.5", "power", "Analysis", "3D", "Depth-gain exponent applied in 3D analysis/visualization."), + + _cli_option("aberrator", "none", "none|water|skull", "Medium", "PAM", "Selects homogeneous water/no aberrator or a CT-derived skull medium."; choices=["none", "water", "skull"]), + _cli_option("ct-path", DEFAULT_CT_PATH, "path", "Medium", "skull", "Path to the private DICOM folder used for CT-backed skull media."), + _cli_option("slice-index", "250", "integer", "Medium", "skull", "CT slice index used when building the skull medium."), + _cli_option("skull-transducer-distance-mm", "30", "mm", "Medium", "skull", "Distance from the receiver/transducer plane to the outer skull surface."), + _cli_option("hu-bone-thr", "200", "HU", "Medium", "skull", "Hounsfield-unit threshold used to identify bone in CT data."), + + _cli_option("simulation-backend", "kwave", "kwave|analytic", "Simulation", "PAM", "Forward model backend. CT skull runs require k-Wave."; choices=["kwave", "analytic"]), + _cli_option("kwave-use-gpu", "true", "bool", "Simulation", "k-Wave", "Passes GPU execution to k-Wave where supported."), + _cli_option("recon-use-gpu", "true", "bool", "Reconstruction", "PAM", "Uses the CUDA.jl reconstruction backend. 3D reconstruction currently requires this to be true."), + _cli_option("recon-bandwidth-khz", "500", "kHz", "Reconstruction", "PAM", "Half-width bandwidth used to select frequency bins around reconstruction frequencies. Use 0 to keep only the target bins."), + _cli_option("recon-step-um", "50", "micrometers", "Reconstruction", "PAM", "Axial integration step used by ASA/HASA reconstruction."), + _cli_option("recon-mode", "auto", "auto|full|windowed", "Reconstruction", "PAM", "Reconstruction mode. Auto uses full for point sources and windowed for squiggle/network activity."; choices=["auto", "full", "windowed"]), + _cli_option("recon-window-us", "20", "microseconds", "Reconstruction", "windowed", "Window duration for windowed incoherent reconstruction."), + _cli_option("recon-hop-us", "10", "microseconds", "Reconstruction", "windowed", "Hop between consecutive reconstruction windows."), + _cli_option("recon-window-taper", "hann", "hann|none|rectangular|tukey", "Reconstruction", "windowed", "Taper applied to each reconstruction window."; choices=["hann", "none", "rectangular", "tukey"]), + _cli_option("recon-min-window-energy-ratio", "0.001", "ratio", "Reconstruction", "windowed", "Skips windows whose energy is below this fraction of the maximum window energy."), + _cli_option("recon-progress", "false", "bool", "Reconstruction", "PAM", "Prints reconstruction progress updates."), + _cli_option("window-batch", "1", "integer", "Reconstruction", "windowed GPU", "Number of reconstruction windows batched together on the GPU."), + + _cli_option("analysis-mode", "auto", "auto|localization|detection", "Analysis", "PAM", "Selects localization or activity-detection metrics. Auto uses detection for squiggle/network sources."; choices=["auto", "localization", "detection"]), + _cli_option("detection-threshold-ratio", "0.2", "ratio", "Analysis", "detection", "Single threshold ratio used by basic detection analysis."), + _cli_option("boundary-threshold-ratios", "0.5,0.55,0.6,0.65,0.7,0.75", "comma list", "Analysis", "detection", "Threshold ratios used for boundary overlays and threshold sweeps."), + _cli_option("auto-threshold-search", "true", "bool", "Analysis", "detection", "Searches a dense threshold range and selects representative detection thresholds."), + _cli_option("auto-threshold-min", "0.10", "ratio", "Analysis", "detection", "Minimum threshold ratio for automatic threshold search."), + _cli_option("auto-threshold-max", "0.95", "ratio", "Analysis", "detection", "Maximum threshold ratio for automatic threshold search."), + _cli_option("auto-threshold-step", "0.01", "ratio", "Analysis", "detection", "Threshold ratio spacing for automatic threshold search."), + ] +end + +function pam_cli_defaults() + return Dict(option.name => option.default for option in pam_cli_options()) +end + +function parse_cli(args) + opts = pam_cli_defaults() + + provided_keys = Set{String}() + for arg in args + startswith(arg, "--") || error("Unsupported argument format: $arg") + parts = split(arg[3:end], "="; limit=2) + length(parts) == 2 || error("Arguments must use --name=value, got: $arg") + push!(provided_keys, parts[1]) + opts[parts[1]] = parts[2] + end + apply_model_defaults!(opts, provided_keys) + return opts, provided_keys +end + +slug_value(x; digits::Int=1) = replace(string(round(Float64(x); digits=digits)), "-" => "m", "." => "p") +parse_bool(s::AbstractString) = lowercase(strip(s)) in ("1", "true", "yes", "on") + +function parse_dimension(s::AbstractString) + value = strip(s) + value in ("2", "2d", "2D") && return 2 + value in ("3", "3d", "3D") && return 3 + error("--dimension must be 2 or 3, got: $s") +end + +function parse_float_list(spec::AbstractString) + isempty(strip(spec)) && return Float64[] + return [parse(Float64, strip(item)) for item in split(spec, ",") if !isempty(strip(item))] +end + +function parse_int_list(spec::AbstractString) + isempty(strip(spec)) && return Int[] + return [parse(Int, strip(item)) for item in split(spec, ",") if !isempty(strip(item))] +end + +function parse_threshold_ratios(spec::AbstractString) + ratios = parse_float_list(spec) + isempty(ratios) && error("At least one threshold ratio is required.") + all(r -> r > 0, ratios) || error("Threshold ratios must be positive.") + return sort(unique(ratios)) +end + +function parse_threshold_search_ratios(opts) + min_ratio = parse(Float64, opts["auto-threshold-min"]) + max_ratio = parse(Float64, opts["auto-threshold-max"]) + step = parse(Float64, opts["auto-threshold-step"]) + min_ratio > 0 || error("--auto-threshold-min must be positive.") + max_ratio >= min_ratio || error("--auto-threshold-max must be >= --auto-threshold-min.") + step > 0 || error("--auto-threshold-step must be positive.") + n = floor(Int, (max_ratio - min_ratio) / step + 1e-9) + ratios = [round(min_ratio + i * step; digits=6) for i in 0:n] + if isempty(ratios) || ratios[end] < max_ratio - 1e-9 + push!(ratios, round(max_ratio; digits=6)) + end + return sort(unique(ratios)) +end + +function parse_source_model(s::AbstractString) + value = Symbol(lowercase(strip(s))) + value in (:point, :squiggle, :network) || error("--source-model must be point, squiggle, or network, got: $s") + return value +end + +function apply_model_defaults!(opts, provided_keys::Set{String}) + dimension = parse_dimension(opts["dimension"]) + if dimension == 3 + !("source-model" in provided_keys) && (opts["source-model"] = "point") + !("sources-mm" in provided_keys) && (opts["sources-mm"] = "30:0:0") + !("anchors-mm" in provided_keys) && (opts["anchors-mm"] = "45:0:0") + !("vascular-squiggle-amplitude-x-mm" in provided_keys) && (opts["vascular-squiggle-amplitude-x-mm"] = "1.0") + !("squiggle-phase-x-deg" in provided_keys) && (opts["squiggle-phase-x-deg"] = "90") + !("frequency-mhz" in provided_keys) && (opts["frequency-mhz"] = "0.5") + !("recon-bandwidth-khz" in provided_keys) && (opts["recon-bandwidth-khz"] = "0") + !("receiver-aperture-mm" in provided_keys) && (opts["receiver-aperture-mm"] = "full") + !("dx-mm" in provided_keys) && (opts["dx-mm"] = "0.2") + !("dy-mm" in provided_keys) && (opts["dy-mm"] = "0.5") + !("dz-mm" in provided_keys) && (opts["dz-mm"] = "0.5") + !("axial-mm" in provided_keys) && (opts["axial-mm"] = "60") + !("transverse-mm" in provided_keys) && (opts["transverse-mm"] = "32") + !("dt-ns" in provided_keys) && (opts["dt-ns"] = "80") + !("t-max-us" in provided_keys) && (opts["t-max-us"] = "60") + !("zero-pad-factor" in provided_keys) && (opts["zero-pad-factor"] = "4") + !("num-cycles" in provided_keys) && (opts["num-cycles"] = "5") + !("phase-mode" in provided_keys) && (opts["phase-mode"] = "coherent") + !("recon-step-um" in provided_keys) && (opts["recon-step-um"] = "50") + end + source_model = parse_source_model(opts["source-model"]) + if dimension == 3 && source_model in (:squiggle, :network) + !("vascular-source-spacing-mm" in provided_keys) && (opts["vascular-source-spacing-mm"] = "0.5") + !("vascular-min-separation-mm" in provided_keys) && (opts["vascular-min-separation-mm"] = "0.25") + !("recon-bandwidth-khz" in provided_keys) && (opts["recon-bandwidth-khz"] = "40") + !("recon-window-us" in provided_keys) && (opts["recon-window-us"] = "40") + !("recon-hop-us" in provided_keys) && (opts["recon-hop-us"] = "20") + !("boundary-threshold-ratios" in provided_keys) && (opts["boundary-threshold-ratios"] = "0.5,0.55,0.6,0.65,0.7,0.75") + end + if source_model == :point + !("source-phase-mode" in provided_keys) && (opts["source-phase-mode"] = "coherent") + !("recon-bandwidth-khz" in provided_keys) && (opts["recon-bandwidth-khz"] = "0") + !("receiver-aperture-mm" in provided_keys) && (opts["receiver-aperture-mm"] = "50") + !("transverse-mm" in provided_keys) && (opts["transverse-mm"] = "60") + !("dt-ns" in provided_keys) && (opts["dt-ns"] = "40") + !("t-max-us" in provided_keys) && (opts["t-max-us"] = "60") + !("axial-mm" in provided_keys) && (opts["axial-mm"] = "60") + !("phase-mode" in provided_keys) && (opts["phase-mode"] = "coherent") + end + return opts +end + +function parse_aberrator(s::AbstractString) + value = Symbol(lowercase(strip(s))) + value in (:none, :water, :skull) || error("Unknown aberrator: $s") + return value +end + +function parse_simulation_backend(s::AbstractString) + value = Symbol(lowercase(strip(s))) + value in (:analytic, :kwave) || error("Unknown --simulation-backend: $s (must be analytic or kwave)") + return value +end + +function parse_source_phase_mode(s::AbstractString) + value = Symbol(replace(lowercase(strip(s)), "-" => "_")) + value in (:coherent, :random_static_phase, :random_phase_per_window) || + error("--source-phase-mode must be coherent, random_static_phase, or random_phase_per_window, got: $s") + return value +end + +parse_source_variability(opts) = SourceVariabilityConfig( + frequency_jitter_fraction=parse(Float64, opts["frequency-jitter-percent"]) / 100.0, +) + +function source_variability_from_summary(summary) + if isnothing(summary) || !hasproperty(summary, :source_variability) + return SourceVariabilityConfig() + end + sv = summary.source_variability + if hasproperty(sv, :frequency_jitter_percent) + return SourceVariabilityConfig(frequency_jitter_fraction=Float64(sv.frequency_jitter_percent) / 100.0) + end + return SourceVariabilityConfig() +end + +function parse_analysis_mode(s::AbstractString, source_model::Symbol) + value = Symbol(lowercase(strip(s))) + value == :auto && return source_model in (:squiggle, :network) ? :detection : :localization + value in (:localization, :detection) || error("--analysis-mode must be auto, localization, or detection, got: $s") + return value +end + +resolve_reconstruction_mode(s::AbstractString, source_model::Symbol) = + TranscranialFUS.pam_reconstruction_mode(s, source_model) + +function parse_window_taper(s::AbstractString) + value = Symbol(replace(lowercase(strip(s)), "-" => "_")) + value in (:hann, :none, :rect, :rectangular, :tukey) || + error("--recon-window-taper must be hann, none, rectangular, or tukey, got: $s") + return value +end + +function make_window_config(opts, reconstruction_mode::Symbol) + return PAMWindowConfig( + enabled=reconstruction_mode == :windowed, + window_duration=parse(Float64, opts["recon-window-us"]) * 1e-6, + hop=parse(Float64, opts["recon-hop-us"]) * 1e-6, + taper=parse_window_taper(opts["recon-window-taper"]), + min_energy_ratio=parse(Float64, opts["recon-min-window-energy-ratio"]), + accumulation=:intensity, + ) +end + +function parse_receiver_aperture_mm(s::AbstractString) + value = lowercase(strip(s)) + value in ("none", "full", "all") && return nothing + return parse(Float64, value) * 1e-3 +end + +function parse_transducer_mm(s::AbstractString) + parts = split(strip(s), ":"; limit=2) + length(parts) == 2 || error("--transducer-mm must be depth_mm:lateral_mm, got: $s") + return parse(Float64, strip(parts[1])) * 1e-3, parse(Float64, strip(parts[2])) * 1e-3 +end + +function default_output_dir(opts, sources, cfg, emission_meta) + timestamp = Dates.format(Dates.now(), "yyyymmdd_HHMMSS") + source_model = lowercase(String(emission_meta["source_model"])) + lateral_slug = if cfg isa PAMConfig3D + "laty$(slug_value(cfg.transverse_dim_y * 1e3; digits=0))mm_latz$(slug_value(cfg.transverse_dim_z * 1e3; digits=0))mm" + else + "lat$(slug_value(cfg.transverse_dim * 1e3; digits=0))mm" + end + parts = String[ + timestamp, + "run_pam", + cfg isa PAMConfig3D ? "3d" : "2d", + lowercase(opts["aberrator"]), + source_model, + "$(length(sources))src", + "ax$(slug_value(cfg.axial_dim * 1e3; digits=0))mm", + lateral_slug, + ] + if occursin("squiggle", source_model) || occursin("network", source_model) + count_key = haskey(emission_meta, "n_anchor_clusters") ? "n_anchor_clusters" : "n_network_centers" + label = occursin("network", source_model) ? "centers" : "anchors" + insert!(parts, 5, "$(emission_meta[count_key])$(label)") + push!(parts, "f$(slug_value(parse(Float64, opts["fundamental-mhz"]); digits=2))mhz") + push!(parts, "h$(replace(opts["harmonics"], "," => ""))") + push!(parts, replace(lowercase(opts["source-phase-mode"]), "_" => "")) + else + push!(parts, "f$(slug_value(parse(Float64, opts["frequency-mhz"]); digits=2))mhz") + end + if lowercase(opts["aberrator"]) == "skull" + insert!(parts, length(parts), "slice" * opts["slice-index"]) + insert!(parts, length(parts), "st$(slug_value(parse(Float64, opts["skull-transducer-distance-mm"]); digits=1))mm") + end + return joinpath(pwd(), "outputs", join(parts, "_")) +end + +function default_reconstruction_output_dir(source_dir::AbstractString) + timestamp = Dates.format(Dates.now(), "yyyymmdd_HHMMSS") + source_name = basename(normpath(source_dir)) + return joinpath(pwd(), "outputs", "$(timestamp)_reconstruct_$(source_name)") +end + +function reject_cached_simulation_options!(provided_keys::Set{String}, blocked_keys) + illegal = sort(collect(intersect(provided_keys, Set(blocked_keys)))) + isempty(illegal) && return nothing + formatted = join(["--$key" for key in illegal], ", ") + error("--from-run-dir reuses the previous RF simulation, medium, sources, and grid. Remove simulation-specific option(s): $formatted") +end diff --git a/src/pam/setup/medium.jl b/src/pam/setup/medium.jl new file mode 100644 index 0000000..2fc07e3 --- /dev/null +++ b/src/pam/setup/medium.jl @@ -0,0 +1,60 @@ +# Simulation defaults and lightweight analytic helpers for PAM runner scripts. + +function default_simulation_info(cfg::PAMConfig) + return Dict{Symbol, Any}( + :receiver_row => receiver_row(cfg), + :receiver_cols => receiver_col_range(cfg), + :source_indices => Tuple{Int, Int}[], + ) +end + +function default_simulation_info(cfg::PAMConfig3D) + return Dict{Symbol, Any}( + :receiver_row => receiver_row(cfg), + :receiver_cols_y => receiver_col_range_y(cfg), + :receiver_cols_z => receiver_col_range_z(cfg), + :source_indices => NTuple{3, Int}[], + ) +end + +function default_recon_frequencies(sources) + freqs = Float64[] + for src in sources + append!(freqs, emission_frequencies(src)) + end + return sort(unique(freqs)) +end + +function _sample_source_signal(signal::AbstractVector{<:Real}, t::Real, dt::Real) + u = Float64(t) / Float64(dt) + 1.0 + i0 = floor(Int, u) + i0 < 1 && return 0.0 + i0 > length(signal) && return 0.0 + i0 == length(signal) && return Float64(signal[i0]) + frac = u - i0 + return (1.0 - frac) * Float64(signal[i0]) + frac * Float64(signal[i0 + 1]) +end + +function analytic_rf_for_point_sources_3d(cfg::PAMConfig3D, sources::AbstractVector{<:EmissionSource3D}) + grid = pam_grid_3d(cfg) + ny, nz, nt = pam_Ny(cfg), pam_Nz(cfg), pam_Nt(cfg) + rf = zeros(Float32, ny, nz, nt) + for src in sources + source_signal = TranscranialFUS._source_signal(nt, cfg.dt, src) + for iy in 1:ny, iz in 1:nz + dy_src = grid.y[iy] - src.lateral_y + dz_src = grid.z[iz] - src.lateral_z + r = sqrt(src.depth^2 + dy_src^2 + dz_src^2) + for it in 1:nt + emission_t = (it - 1) * cfg.dt - r / cfg.c0 + rf[iy, iz, it] += Float32(_sample_source_signal(source_signal, emission_t, cfg.dt)) + end + end + end + return rf, grid, Dict{Symbol, Any}( + :receiver_row => receiver_row(cfg), + :receiver_cols_y => receiver_col_range_y(cfg), + :receiver_cols_z => receiver_col_range_z(cfg), + :source_indices => [source_grid_index_3d(src, cfg) for src in sources], + ) +end diff --git a/src/pam/setup/runner.jl b/src/pam/setup/runner.jl new file mode 100644 index 0000000..69dc5e3 --- /dev/null +++ b/src/pam/setup/runner.jl @@ -0,0 +1,158 @@ +# Testable runner planning helpers for scripts/run_pam.jl. + +function run_pam_medium_summary(medium_info) + medium_summary = Dict{String, Any}() + for (key, value) in medium_info + key == :mask && continue + medium_summary[String(key)] = value + end + return medium_summary +end + +function run_pam_dry_plan(args::AbstractVector{<:AbstractString}) + opts, provided_keys = parse_cli(String.(args)) + dimension = parse_dimension(opts["dimension"]) + source_model = parse_source_model(opts["source-model"]) + from_run_dir = strip(opts["from-run-dir"]) + detection_truth_radius_m = parse(Float64, opts["vascular-radius-mm"]) * 1e-3 + boundary_threshold_ratios = parse_threshold_ratios(opts["boundary-threshold-ratios"]) + auto_threshold_search = parse_bool(opts["auto-threshold-search"]) + threshold_score_ratios = auto_threshold_search ? parse_threshold_search_ratios(opts) : boundary_threshold_ratios + + if dimension == 3 + isempty(from_run_dir) || error("--from-run-dir is not implemented for 3D PAM yet.") + source_model in (:point, :squiggle, :network) || + error("3D PAM CLI supports --source-model=point, --source-model=squiggle, or --source-model=network.") + aberrator = parse_aberrator(opts["aberrator"]) + aberrator in (:none, :skull) || error("3D PAM CLI currently supports only --aberrator=none or --aberrator=skull.") + + dy_mm = isempty(strip(opts["dy-mm"])) ? parse(Float64, opts["dz-mm"]) : parse(Float64, opts["dy-mm"]) + transverse_y_mm = isempty(strip(opts["transverse-y-mm"])) ? parse(Float64, opts["transverse-mm"]) : parse(Float64, opts["transverse-y-mm"]) + transverse_z_mm = isempty(strip(opts["transverse-z-mm"])) ? parse(Float64, opts["transverse-mm"]) : parse(Float64, opts["transverse-z-mm"]) + receiver_aperture_y_spec = isempty(strip(opts["receiver-aperture-y-mm"])) ? opts["receiver-aperture-mm"] : opts["receiver-aperture-y-mm"] + receiver_aperture_z_spec = isempty(strip(opts["receiver-aperture-z-mm"])) ? opts["receiver-aperture-mm"] : opts["receiver-aperture-z-mm"] + + cfg_base = PAMConfig3D( + dx=parse(Float64, opts["dx-mm"]) * 1e-3, + dy=dy_mm * 1e-3, + dz=parse(Float64, opts["dz-mm"]) * 1e-3, + axial_dim=parse(Float64, opts["axial-mm"]) * 1e-3, + transverse_dim_y=transverse_y_mm * 1e-3, + transverse_dim_z=transverse_z_mm * 1e-3, + t_max=parse(Float64, opts["t-max-us"]) * 1e-6, + dt=parse(Float64, opts["dt-ns"]) * 1e-9, + zero_pad_factor=parse(Int, opts["zero-pad-factor"]), + receiver_aperture_y=parse_receiver_aperture_mm(receiver_aperture_y_spec), + receiver_aperture_z=parse_receiver_aperture_mm(receiver_aperture_z_spec), + peak_suppression_radius=parse(Float64, opts["peak-suppression-radius-mm"]) * 1e-3, + success_tolerance=parse(Float64, opts["success-tolerance-mm"]) * 1e-3, + axial_gain_power=parse(Float64, opts["axial-gain-power"]), + ) + + sources, emission_meta = if source_model == :point + parse_point_sources_3d(opts) + elseif source_model == :network + parse_network_sources_3d(opts, cfg_base) + else + parse_squiggle_sources_3d(opts, cfg_base) + end + cfg = fit_pam_config_3d(cfg_base, sources; min_bottom_margin=parse(Float64, opts["bottom-margin-mm"]) * 1e-3) + out_dir = if haskey(opts, "out-dir") && !isempty(strip(opts["out-dir"])) + opts["out-dir"] + else + default_output_dir(opts, sources, cfg, emission_meta) + end + simulation_backend = parse_simulation_backend(opts["simulation-backend"]) + simulation_backend == :analytic && aberrator == :skull && + error("--simulation-backend=analytic is not compatible with --aberrator=skull; use --simulation-backend=kwave.") + return Dict( + :branch => :pam3d, + :out_dir => out_dir, + :source_model => source_model, + :source_count => length(sources), + :threshold_score_ratios => threshold_score_ratios, + :detection_truth_radius_m => detection_truth_radius_m, + :simulation_backend => simulation_backend, + ) + end + + if isempty(from_run_dir) + cfg_base = PAMConfig( + dx=parse(Float64, opts["dx-mm"]) * 1e-3, + dz=parse(Float64, opts["dz-mm"]) * 1e-3, + axial_dim=parse(Float64, opts["axial-mm"]) * 1e-3, + transverse_dim=parse(Float64, opts["transverse-mm"]) * 1e-3, + receiver_aperture=parse_receiver_aperture_mm(opts["receiver-aperture-mm"]), + t_max=parse(Float64, opts["t-max-us"]) * 1e-6, + dt=parse(Float64, opts["dt-ns"]) * 1e-9, + zero_pad_factor=parse(Int, opts["zero-pad-factor"]), + peak_suppression_radius=parse(Float64, opts["peak-suppression-radius-mm"]) * 1e-3, + success_tolerance=parse(Float64, opts["success-tolerance-mm"]) * 1e-3, + ) + sources, emission_meta = parse_sources(opts, cfg_base) + source_model = source_model_from_meta(emission_meta, sources) + aberrator = parse_aberrator(opts["aberrator"]) + cfg = fit_pam_config( + cfg_base, + sources; + min_bottom_margin=parse(Float64, opts["bottom-margin-mm"]) * 1e-3, + reference_depth=aberrator == :skull ? parse(Float64, opts["skull-transducer-distance-mm"]) * 1e-3 : nothing, + ) + out_dir = if haskey(opts, "out-dir") && !isempty(strip(opts["out-dir"])) + opts["out-dir"] + else + default_output_dir(opts, sources, cfg, emission_meta) + end + return Dict( + :branch => :pam2d_simulation, + :out_dir => out_dir, + :source_model => source_model, + :source_count => length(sources), + :threshold_ratios => boundary_threshold_ratios, + :detection_truth_radius_m => detection_truth_radius_m, + ) + end + + reject_cached_simulation_options!( + provided_keys, + ( + "source-model", "sources-mm", "anchors-mm", "frequency-mhz", "fundamental-mhz", + "amplitude-pa", "source-amplitudes-pa", "source-frequencies-mhz", "phases-deg", + "num-cycles", "harmonics", "harmonic-amplitudes", + "gate-us", "taper-ratio", "axial-mm", "transverse-mm", "dx-mm", "dz-mm", + "receiver-aperture-mm", "t-max-us", "dt-ns", "zero-pad-factor", + "peak-suppression-radius-mm", "success-tolerance-mm", "aberrator", "ct-path", + "slice-index", "skull-transducer-distance-mm", "bottom-margin-mm", "hu-bone-thr", + "simulation-backend", "phase-mode", "phase-jitter-rad", "random-seed", + "transducer-mm", "delays-us", "vascular-length-mm", "vascular-squiggle-amplitude-mm", + "vascular-squiggle-amplitude-x-mm", "vascular-squiggle-wavelength-mm", + "vascular-squiggle-slope", "squiggle-phase-x-deg", + "vascular-source-spacing-mm", "vascular-position-jitter-mm", + "vascular-min-separation-mm", "vascular-max-sources-per-anchor", + "network-axial-radius-mm", "network-lateral-y-radius-mm", + "network-lateral-z-radius-mm", "network-root-count", "network-generations", + "network-branch-length-mm", "network-branch-step-mm", "network-branch-angle-deg", + "network-tortuosity", "network-orientation", "network-density-sigma-mm", "network-density-axial-sigma-mm", + "network-density-lateral-y-sigma-mm", "network-density-lateral-z-sigma-mm", + "network-max-sources-per-center", + "source-phase-mode", "frequency-jitter-percent", + ), + ) + cached_path = joinpath(from_run_dir, "result.jld2") + isfile(cached_path) || error("--from-run-dir must contain result.jld2, missing: $cached_path") + out_dir = if haskey(opts, "out-dir") && !isempty(strip(opts["out-dir"])) + opts["out-dir"] + else + default_reconstruction_output_dir(from_run_dir) + end + return Dict( + :branch => :pam2d_cached, + :out_dir => out_dir, + :cached_path => cached_path, + :reconstruction_source => Dict( + "mode" => "cached_rf", + "from_run_dir" => abspath(from_run_dir), + "from_result_jld2" => abspath(cached_path), + ), + ) +end diff --git a/src/pam/setup/sources.jl b/src/pam/setup/sources.jl new file mode 100644 index 0000000..2d5cca8 --- /dev/null +++ b/src/pam/setup/sources.jl @@ -0,0 +1,474 @@ +# Source parsing and source metadata helpers for PAM runner scripts. + +point_pairs_m(points) = [[point[1], point[2]] for point in points] +centerlines_m(centerlines) = [point_pairs_m(line) for line in centerlines] + +function expand_source_values(values::Vector{Float64}, n::Int, default::Float64) + isempty(values) && return fill(default, n) + length(values) == 1 && return fill(values[1], n) + length(values) == n && return values + error("Per-source parameter list must have length 1 or match the number of sources ($n).") +end + +function parse_coordinate_pairs_mm(spec::AbstractString, option_name::AbstractString) + coord_tokens = [strip(token) for token in split(spec, ",") if !isempty(strip(token))] + 1 <= length(coord_tokens) <= 20 || error("Provide between 1 and 20 coordinates via --$option_name=depth:lateral,...") + pairs = Tuple{Float64, Float64}[] + for token in coord_tokens + parts = split(token, ":"; limit=2) + length(parts) == 2 || error("Each coordinate must be depth_mm:lateral_mm, got: $token") + push!(pairs, (parse(Float64, strip(parts[1])) * 1e-3, parse(Float64, strip(parts[2])) * 1e-3)) + end + return pairs +end + +function parse_coordinate_triples_mm(spec::AbstractString, option_name::AbstractString) + coord_tokens = [strip(token) for token in split(spec, ",") if !isempty(strip(token))] + 1 <= length(coord_tokens) <= 20 || error("Provide between 1 and 20 coordinates via --$option_name=depth:y:z,...") + triples = NTuple{3, Float64}[] + for token in coord_tokens + parts = split(token, ":"; limit=3) + length(parts) == 3 || error("3D coordinates must be depth:y:z in mm, got: $token") + push!(triples, ( + parse(Float64, strip(parts[1])) * 1e-3, + parse(Float64, strip(parts[2])) * 1e-3, + parse(Float64, strip(parts[3])) * 1e-3, + )) + end + return triples +end + +function parse_point_sources(opts) + coordinates = parse_coordinate_pairs_mm(opts["sources-mm"], "sources-mm") + n_sources = length(coordinates) + frequencies_mhz = expand_source_values( + parse_float_list(opts["source-frequencies-mhz"]), + n_sources, + parse(Float64, opts["frequency-mhz"]), + ) + amplitudes_pa = expand_source_values( + parse_float_list(opts["source-amplitudes-pa"]), + n_sources, + parse(Float64, opts["amplitude-pa"]), + ) + phases_deg = expand_source_values(parse_float_list(opts["phases-deg"]), n_sources, 0.0) + delays_us = expand_source_values(parse_float_list(opts["delays-us"]), n_sources, 0.0) + num_cycles = parse(Int, opts["num-cycles"]) + + phase_mode = lowercase(strip(opts["phase-mode"])) + phase_mode in ("coherent", "random", "jittered") || + error("Point --phase-mode must be coherent, random, or jittered, got: $phase_mode") + rng = Random.MersenneTwister(parse(Int, opts["random-seed"])) + if phase_mode == "random" + phases_deg = rand(rng, n_sources) .* 360.0 + elseif phase_mode == "jittered" + phases_deg = phases_deg .+ randn(rng, n_sources) .* (parse(Float64, opts["phase-jitter-rad"]) * 180 / pi) + end + + sources = EmissionSource2D[] + for (idx, (depth_m, lateral_m)) in pairs(coordinates) + push!(sources, PointSource2D( + depth=depth_m, + lateral=lateral_m, + frequency=frequencies_mhz[idx] * 1e6, + amplitude=amplitudes_pa[idx], + phase=phases_deg[idx] * pi / 180, + delay=delays_us[idx] * 1e-6, + num_cycles=num_cycles, + )) + end + return sources, Dict{String, Any}( + "source_model" => "point", + "coordinates_m" => [collect(coord) for coord in coordinates], + "n_coordinate_sources" => n_sources, + "n_emission_sources" => length(sources), + "physical_source_count" => length(sources), + "emission_event_count" => length(sources), + "activity_model" => Dict("activity_mode" => "point_tone_burst"), + "phase_mode" => phase_mode, + "frequencies_hz" => frequencies_mhz .* 1e6, + "amplitudes_pa" => amplitudes_pa, + "phases_rad" => phases_deg .* pi ./ 180, + "delays_s" => delays_us .* 1e-6, + "num_cycles" => num_cycles, + "random_seed" => parse(Int, opts["random-seed"]), + ) +end + +function parse_point_sources_3d(opts) + coordinates = parse_coordinate_triples_mm(opts["sources-mm"], "sources-mm") + n_sources = length(coordinates) + frequencies_mhz = expand_source_values( + parse_float_list(opts["source-frequencies-mhz"]), + n_sources, + parse(Float64, opts["frequency-mhz"]), + ) + amplitudes_pa = expand_source_values( + parse_float_list(opts["source-amplitudes-pa"]), + n_sources, + parse(Float64, opts["amplitude-pa"]), + ) + phases_deg = expand_source_values(parse_float_list(opts["phases-deg"]), n_sources, 0.0) + delays_us = expand_source_values(parse_float_list(opts["delays-us"]), n_sources, 0.0) + num_cycles = parse(Int, opts["num-cycles"]) + + phase_mode = lowercase(strip(opts["phase-mode"])) + phase_mode in ("coherent", "random", "jittered") || + error("Point --phase-mode must be coherent, random, or jittered, got: $phase_mode") + rng = Random.MersenneTwister(parse(Int, opts["random-seed"])) + if phase_mode == "random" + phases_deg = rand(rng, n_sources) .* 360.0 + elseif phase_mode == "jittered" + phases_deg = phases_deg .+ randn(rng, n_sources) .* (parse(Float64, opts["phase-jitter-rad"]) * 180 / pi) + end + + sources = EmissionSource3D[] + for (idx, (depth_m, lateral_y_m, lateral_z_m)) in pairs(coordinates) + push!(sources, PointSource3D( + depth=depth_m, + lateral_y=lateral_y_m, + lateral_z=lateral_z_m, + frequency=frequencies_mhz[idx] * 1e6, + amplitude=amplitudes_pa[idx], + phase=phases_deg[idx] * pi / 180, + delay=delays_us[idx] * 1e-6, + num_cycles=num_cycles, + )) + end + return sources, Dict{String, Any}( + "source_model" => "point3d", + "coordinates_m" => [collect(coord) for coord in coordinates], + "n_coordinate_sources" => n_sources, + "n_emission_sources" => length(sources), + "physical_source_count" => length(sources), + "emission_event_count" => length(sources), + "activity_model" => Dict("activity_mode" => "point_tone_burst_3d"), + "phase_mode" => phase_mode, + "frequencies_hz" => frequencies_mhz .* 1e6, + "amplitudes_pa" => amplitudes_pa, + "phases_rad" => phases_deg .* pi ./ 180, + "delays_s" => delays_us .* 1e-6, + "num_cycles" => num_cycles, + "random_seed" => parse(Int, opts["random-seed"]), + ) +end + +centerlines_m_3d(centerlines) = [[[p[1], p[2], p[3]] for p in line] for line in centerlines] + +function parse_squiggle_sources_3d(opts, cfg::PAMConfig3D) + anchors = parse_coordinate_triples_mm(opts["anchors-mm"], "anchors-mm") + f0 = parse(Float64, opts["fundamental-mhz"]) * 1e6 + harmonics = parse_int_list(opts["harmonics"]) + isempty(harmonics) && error("--harmonics must be a non-empty integer list.") + harmonic_amplitudes = parse_float_list(opts["harmonic-amplitudes"]) + length(harmonic_amplitudes) == length(harmonics) || + error("--harmonic-amplitudes must have the same length as --harmonics ($(length(harmonics))).") + + n_anchors = length(anchors) + delays_us = expand_source_values(parse_float_list(opts["delays-us"]), n_anchors, 0.0) + max_sources_raw = parse(Int, opts["vascular-max-sources-per-anchor"]) + max_sources = max_sources_raw <= 0 ? nothing : max_sources_raw + phase_mode = Symbol(replace(lowercase(strip(opts["phase-mode"])), "-" => "_")) + + sources = EmissionSource3D[] + all_centerlines_m = Any[] + anchors_meta = Dict{String, Any}[] + rng = Random.MersenneTwister(parse(Int, opts["random-seed"])) + half_y = cfg.transverse_dim_y / 2 + half_z = cfg.transverse_dim_z / 2 + + for (idx, anchor) in pairs(anchors) + anchor_sources, anchor_meta = make_squiggle_bubble_sources_3d( + [anchor]; + root_length = parse(Float64, opts["vascular-length-mm"]) * 1e-3, + squiggle_amplitude_y = parse(Float64, opts["vascular-squiggle-amplitude-mm"]) * 1e-3, + squiggle_amplitude_x = parse(Float64, opts["vascular-squiggle-amplitude-x-mm"]) * 1e-3, + squiggle_wavelength = parse(Float64, opts["vascular-squiggle-wavelength-mm"]) * 1e-3, + squiggle_phase_x = parse(Float64, opts["squiggle-phase-x-deg"]) * pi / 180, + squiggle_slope_x = parse(Float64, opts["vascular-squiggle-slope"]), + squiggle_slope_y = 0.0, + source_spacing = parse(Float64, opts["vascular-source-spacing-mm"]) * 1e-3, + position_jitter = parse(Float64, opts["vascular-position-jitter-mm"]) * 1e-3, + min_separation = parse(Float64, opts["vascular-min-separation-mm"]) * 1e-3, + max_sources_per_anchor = max_sources, + depth_bounds = (0.0, Inf), + lateral_y_bounds = (-half_y, half_y), + lateral_z_bounds = (-half_z, half_z), + fundamental = f0, + amplitude = parse(Float64, opts["amplitude-pa"]), + harmonics = harmonics, + harmonic_amplitudes = harmonic_amplitudes, + gate_duration = parse(Float64, opts["gate-us"]) * 1e-6, + taper_ratio = parse(Float64, opts["taper-ratio"]), + delay = delays_us[idx] * 1e-6, + phase_mode = phase_mode, + phase_jitter = parse(Float64, opts["phase-jitter-rad"]), + transducer_depth = -parse(Float64, opts["skull-transducer-distance-mm"]) * 1e-3, + transducer_y = 0.0, + transducer_z = 0.0, + c0 = cfg.c0, + rng = rng, + ) + append!(sources, anchor_sources) + anchor_cls = centerlines_m_3d(anchor_meta[:centerlines]) + append!(all_centerlines_m, anchor_cls) + push!(anchors_meta, Dict( + "anchor_m" => collect(anchor), + "source_count" => length(anchor_sources), + "centerlines_m" => anchor_cls, + )) + end + + return sources, Dict{String, Any}( + "source_model" => "squiggle3d", + "anchor_clusters_m" => [collect(anchor) for anchor in anchors], + "n_anchor_clusters" => length(anchors), + "n_emission_sources" => length(sources), + "physical_source_count" => length(sources), + "emission_event_count" => length(sources), + "activity_model" => Dict("activity_mode" => "random_phase_per_window"), + "phase_mode" => String(phase_mode), + "fundamental_hz" => f0, + "harmonics" => harmonics, + "harmonic_amplitudes" => harmonic_amplitudes, + "gate_duration_s" => parse(Float64, opts["gate-us"]) * 1e-6, + "phase_jitter_rad" => parse(Float64, opts["phase-jitter-rad"]), + "random_seed" => parse(Int, opts["random-seed"]), + "delays_s" => delays_us .* 1e-6, + "squiggle" => Dict( + "length_m" => parse(Float64, opts["vascular-length-mm"]) * 1e-3, + "squiggle_amplitude_y_m" => parse(Float64, opts["vascular-squiggle-amplitude-mm"]) * 1e-3, + "squiggle_amplitude_x_m" => parse(Float64, opts["vascular-squiggle-amplitude-x-mm"]) * 1e-3, + "squiggle_wavelength_m" => parse(Float64, opts["vascular-squiggle-wavelength-mm"]) * 1e-3, + "squiggle_phase_x_deg" => parse(Float64, opts["squiggle-phase-x-deg"]), + "squiggle_slope" => parse(Float64, opts["vascular-squiggle-slope"]), + "source_spacing_m" => parse(Float64, opts["vascular-source-spacing-mm"]) * 1e-3, + "position_jitter_m" => parse(Float64, opts["vascular-position-jitter-mm"]) * 1e-3, + "min_separation_m" => parse(Float64, opts["vascular-min-separation-mm"]) * 1e-3, + "max_sources_per_anchor" => max_sources_raw, + "truth_radius_m" => parse(Float64, opts["vascular-radius-mm"]) * 1e-3, + "centerlines_m" => all_centerlines_m, + "anchors" => anchors_meta, + ), + ) +end + +function parse_network_sources_3d(opts, cfg::PAMConfig3D) + centers = parse_coordinate_triples_mm(opts["anchors-mm"], "anchors-mm") + f0 = parse(Float64, opts["fundamental-mhz"]) * 1e6 + harmonics = parse_int_list(opts["harmonics"]) + isempty(harmonics) && error("--harmonics must be a non-empty integer list.") + harmonic_amplitudes = parse_float_list(opts["harmonic-amplitudes"]) + length(harmonic_amplitudes) == length(harmonics) || + error("--harmonic-amplitudes must have the same length as --harmonics ($(length(harmonics))).") + + n_centers = length(centers) + delays_us = expand_source_values(parse_float_list(opts["delays-us"]), n_centers, 0.0) + max_sources_raw = parse(Int, opts["network-max-sources-per-center"]) + max_sources = max_sources_raw <= 0 ? nothing : max_sources_raw + phase_mode = Symbol(replace(lowercase(strip(opts["phase-mode"])), "-" => "_")) + axial_radius_m = parse(Float64, opts["network-axial-radius-mm"]) * 1e-3 + lateral_y_radius_m = parse(Float64, opts["network-lateral-y-radius-mm"]) * 1e-3 + lateral_z_radius_m = parse(Float64, opts["network-lateral-z-radius-mm"]) * 1e-3 + ellipsoid_radii_m = [axial_radius_m, lateral_y_radius_m, lateral_z_radius_m] + density_sigma_m = parse(Float64, opts["network-density-sigma-mm"]) * 1e-3 + density_axial_sigma_m = parse(Float64, opts["network-density-axial-sigma-mm"]) * 1e-3 + density_lateral_y_sigma_m = parse(Float64, opts["network-density-lateral-y-sigma-mm"]) * 1e-3 + density_lateral_z_sigma_m = parse(Float64, opts["network-density-lateral-z-sigma-mm"]) * 1e-3 + density_sigmas_m = density_sigma_m > 0 ? + [density_sigma_m, density_sigma_m, density_sigma_m] : + [density_axial_sigma_m, density_lateral_y_sigma_m, density_lateral_z_sigma_m] + + sources = EmissionSource3D[] + all_centerlines_m = Any[] + centers_meta = Dict{String, Any}[] + rng = Random.MersenneTwister(parse(Int, opts["random-seed"])) + half_y = cfg.transverse_dim_y / 2 + half_z = cfg.transverse_dim_z / 2 + + for (idx, center) in pairs(centers) + center_sources, network_meta = make_network_bubble_sources_3d( + [center]; + axial_radius = axial_radius_m, + lateral_y_radius = lateral_y_radius_m, + lateral_z_radius = lateral_z_radius_m, + root_count = parse(Int, opts["network-root-count"]), + generations = parse(Int, opts["network-generations"]), + branch_length = parse(Float64, opts["network-branch-length-mm"]) * 1e-3, + branch_step = parse(Float64, opts["network-branch-step-mm"]) * 1e-3, + branch_angle = parse(Float64, opts["network-branch-angle-deg"]) * pi / 180, + tortuosity = parse(Float64, opts["network-tortuosity"]), + network_orientation = Symbol(replace(lowercase(strip(opts["network-orientation"])), "-" => "_")), + source_spacing = parse(Float64, opts["vascular-source-spacing-mm"]) * 1e-3, + density_sigma = density_sigma_m, + density_sigma_depth = density_axial_sigma_m, + density_sigma_y = density_lateral_y_sigma_m, + density_sigma_z = density_lateral_z_sigma_m, + min_separation = parse(Float64, opts["vascular-min-separation-mm"]) * 1e-3, + max_sources_per_center = max_sources, + depth_bounds = (0.0, Inf), + lateral_y_bounds = (-half_y, half_y), + lateral_z_bounds = (-half_z, half_z), + fundamental = f0, + amplitude = parse(Float64, opts["amplitude-pa"]), + harmonics = harmonics, + harmonic_amplitudes = harmonic_amplitudes, + gate_duration = parse(Float64, opts["gate-us"]) * 1e-6, + taper_ratio = parse(Float64, opts["taper-ratio"]), + delay = delays_us[idx] * 1e-6, + phase_mode = phase_mode, + phase_jitter = parse(Float64, opts["phase-jitter-rad"]), + transducer_depth = -parse(Float64, opts["skull-transducer-distance-mm"]) * 1e-3, + transducer_y = 0.0, + transducer_z = 0.0, + c0 = cfg.c0, + rng = rng, + ) + append!(sources, center_sources) + center_cls = centerlines_m_3d(network_meta[:centerlines]) + append!(all_centerlines_m, center_cls) + push!(centers_meta, Dict( + "center_m" => collect(center), + "source_count" => length(center_sources), + "centerline_count" => length(network_meta[:centerlines]), + "centerlines_m" => center_cls, + )) + end + + return sources, Dict{String, Any}( + "source_model" => "network3d", + "network_centers_m" => [collect(center) for center in centers], + "n_network_centers" => length(centers), + "n_emission_sources" => length(sources), + "physical_source_count" => length(sources), + "emission_event_count" => length(sources), + "activity_model" => Dict("activity_mode" => "random_phase_per_window"), + "phase_mode" => String(phase_mode), + "fundamental_hz" => f0, + "harmonics" => harmonics, + "harmonic_amplitudes" => harmonic_amplitudes, + "gate_duration_s" => parse(Float64, opts["gate-us"]) * 1e-6, + "phase_jitter_rad" => parse(Float64, opts["phase-jitter-rad"]), + "random_seed" => parse(Int, opts["random-seed"]), + "delays_s" => delays_us .* 1e-6, + "network" => Dict( + "axial_radius_m" => axial_radius_m, + "lateral_y_radius_m" => lateral_y_radius_m, + "lateral_z_radius_m" => lateral_z_radius_m, + "ellipsoid_radii_m" => ellipsoid_radii_m, + "root_count" => parse(Int, opts["network-root-count"]), + "generations" => parse(Int, opts["network-generations"]), + "branch_length_m" => parse(Float64, opts["network-branch-length-mm"]) * 1e-3, + "branch_step_m" => parse(Float64, opts["network-branch-step-mm"]) * 1e-3, + "branch_angle_deg" => parse(Float64, opts["network-branch-angle-deg"]), + "tortuosity" => parse(Float64, opts["network-tortuosity"]), + "orientation" => opts["network-orientation"], + "density_sigma_m" => density_sigma_m, + "density_sigmas_m" => density_sigmas_m, + "source_spacing_m" => parse(Float64, opts["vascular-source-spacing-mm"]) * 1e-3, + "min_separation_m" => parse(Float64, opts["vascular-min-separation-mm"]) * 1e-3, + "max_sources_per_center" => max_sources_raw, + "truth_radius_m" => parse(Float64, opts["vascular-radius-mm"]) * 1e-3, + "centerlines_m" => all_centerlines_m, + "centers" => centers_meta, + ), + ) +end + +function parse_squiggle_sources(opts, cfg::PAMConfig) + anchors = parse_coordinate_pairs_mm(opts["anchors-mm"], "anchors-mm") + f0 = parse(Float64, opts["fundamental-mhz"]) * 1e6 + harmonics = parse_int_list(opts["harmonics"]) + isempty(harmonics) && error("--harmonics must be a non-empty integer list.") + harmonic_amplitudes = parse_float_list(opts["harmonic-amplitudes"]) + length(harmonic_amplitudes) == length(harmonics) || + error("--harmonic-amplitudes must have the same length as --harmonics ($(length(harmonics))).") + + n_anchors = length(anchors) + delays_us = expand_source_values(parse_float_list(opts["delays-us"]), n_anchors, 0.0) + tx_depth, tx_lateral = parse_transducer_mm(opts["transducer-mm"]) + max_sources_raw = parse(Int, opts["vascular-max-sources-per-anchor"]) + max_sources = max_sources_raw <= 0 ? nothing : max_sources_raw + phase_mode = Symbol(replace(lowercase(strip(opts["phase-mode"])), "-" => "_")) + + sources = EmissionSource2D[] + all_centerlines_m = Any[] + anchors_meta = Dict{String, Any}[] + rng = Random.MersenneTwister(parse(Int, opts["random-seed"])) + for (idx, anchor) in pairs(anchors) + anchor_sources, anchor_meta = make_squiggle_bubble_sources( + [anchor]; + root_length=parse(Float64, opts["vascular-length-mm"]) * 1e-3, + squiggle_amplitude=parse(Float64, opts["vascular-squiggle-amplitude-mm"]) * 1e-3, + squiggle_wavelength=parse(Float64, opts["vascular-squiggle-wavelength-mm"]) * 1e-3, + squiggle_slope=parse(Float64, opts["vascular-squiggle-slope"]), + source_spacing=parse(Float64, opts["vascular-source-spacing-mm"]) * 1e-3, + position_jitter=parse(Float64, opts["vascular-position-jitter-mm"]) * 1e-3, + min_separation=parse(Float64, opts["vascular-min-separation-mm"]) * 1e-3, + max_sources_per_anchor=max_sources, + depth_bounds=(0.0, Inf), + lateral_bounds=(-cfg.transverse_dim / 2, cfg.transverse_dim / 2), + fundamental=f0, + amplitude=parse(Float64, opts["amplitude-pa"]), + harmonics=harmonics, + harmonic_amplitudes=harmonic_amplitudes, + gate_duration=parse(Float64, opts["gate-us"]) * 1e-6, + taper_ratio=parse(Float64, opts["taper-ratio"]), + delay=delays_us[idx] * 1e-6, + phase_mode=phase_mode, + phase_jitter=parse(Float64, opts["phase-jitter-rad"]), + transducer_depth=tx_depth, + transducer_lateral=tx_lateral, + c0=cfg.c0, + rng=rng, + ) + append!(sources, anchor_sources) + anchor_centerlines = centerlines_m(anchor_meta[:centerlines]) + append!(all_centerlines_m, anchor_centerlines) + push!(anchors_meta, Dict( + "anchor_m" => collect(anchor), + "source_count" => length(anchor_sources), + "centerlines_m" => anchor_centerlines, + )) + end + + return sources, Dict{String, Any}( + "source_model" => "squiggle", + "anchor_clusters_m" => [collect(anchor) for anchor in anchors], + "n_anchor_clusters" => length(anchors), + "n_emission_sources" => length(sources), + "physical_source_count" => length(sources), + "emission_event_count" => length(sources), + "activity_model" => Dict("activity_mode" => "random_phase_per_window"), + "phase_mode" => String(phase_mode), + "fundamental_hz" => f0, + "harmonics" => harmonics, + "harmonic_amplitudes" => harmonic_amplitudes, + "gate_duration_s" => parse(Float64, opts["gate-us"]) * 1e-6, + "transducer_m" => [tx_depth, tx_lateral], + "phase_jitter_rad" => parse(Float64, opts["phase-jitter-rad"]), + "random_seed" => parse(Int, opts["random-seed"]), + "delays_s" => delays_us .* 1e-6, + "squiggle" => Dict( + "length_m" => parse(Float64, opts["vascular-length-mm"]) * 1e-3, + "squiggle_amplitude_m" => parse(Float64, opts["vascular-squiggle-amplitude-mm"]) * 1e-3, + "squiggle_wavelength_m" => parse(Float64, opts["vascular-squiggle-wavelength-mm"]) * 1e-3, + "squiggle_slope" => parse(Float64, opts["vascular-squiggle-slope"]), + "source_spacing_m" => parse(Float64, opts["vascular-source-spacing-mm"]) * 1e-3, + "position_jitter_m" => parse(Float64, opts["vascular-position-jitter-mm"]) * 1e-3, + "min_separation_m" => parse(Float64, opts["vascular-min-separation-mm"]) * 1e-3, + "max_sources_per_anchor" => max_sources_raw, + "truth_radius_m" => parse(Float64, opts["vascular-radius-mm"]) * 1e-3, + "centerlines_m" => all_centerlines_m, + "anchors" => anchors_meta, + ), + ) +end + +function parse_sources(opts, cfg::PAMConfig) + source_model = parse_source_model(opts["source-model"]) + source_model == :point && return parse_point_sources(opts) + source_model == :squiggle && return parse_squiggle_sources(opts, cfg) + error("2D PAM CLI supports --source-model=point or --source-model=squiggle.") +end + diff --git a/src/pam/setup/summary.jl b/src/pam/setup/summary.jl new file mode 100644 index 0000000..b7c62bc --- /dev/null +++ b/src/pam/setup/summary.jl @@ -0,0 +1,124 @@ +function json3_to_any(x) + if x isa JSON3.Object + return Dict{String, Any}(String(k) => json3_to_any(v) for (k, v) in pairs(x)) + elseif x isa JSON3.Array + return Any[json3_to_any(v) for v in x] + else + return x + end +end + +function source_model_from_meta(meta, sources) + if haskey(meta, "source_model") + model = Symbol(String(meta["source_model"])) + model == :vascular && return :squiggle + model == :squiggle3d && return :squiggle + model == :network3d && return :network + return model + end + if haskey(meta, "cluster_model") + old = Symbol(String(meta["cluster_model"])) + return old == :vascular ? :squiggle : old + end + return any(src -> src isa Union{BubbleCluster2D, BubbleCluster3D}, sources) ? :squiggle : :point +end + +function centerlines_from_emission_meta(meta) + key = haskey(meta, "squiggle") ? "squiggle" : (haskey(meta, "network") ? "network" : (haskey(meta, "vascular") ? "vascular" : "")) + isempty(key) && return nothing + block = meta[key] + haskey(block, "centerlines_m") || return nothing + centerlines = Vector{Tuple{Float64, Float64}}[] + for raw_line in block["centerlines_m"] + line = Tuple{Float64, Float64}[] + for point in raw_line + push!(line, (Float64(point[1]), Float64(point[2]))) + end + length(line) >= 2 && push!(centerlines, line) + end + return isempty(centerlines) ? nothing : centerlines +end + +function detection_truth_mask_from_meta(meta, kgrid, cfg, radius::Real) + centerlines = centerlines_from_emission_meta(meta) + isnothing(centerlines) && return nothing + return pam_centerline_truth_mask(centerlines, kgrid, cfg; radius=radius) +end + +string_key_dict(d::AbstractDict) = Dict(String(k) => v for (k, v) in d) + +function compact_window_info(info) + haskey(info, :used_window_count) || return nothing + range_pairs(ranges) = [[first(range), last(range)] for range in ranges] + return Dict( + "total_window_count" => info[:total_window_count], + "used_window_count" => info[:used_window_count], + "skipped_window_count" => info[:skipped_window_count], + "window_samples" => info[:window_samples], + "hop_samples" => info[:hop_samples], + "effective_window_duration_s" => get(info, :effective_window_duration_s, nothing), + "effective_hop_s" => get(info, :effective_hop_s, nothing), + "energy_threshold" => info[:energy_threshold], + "used_window_ranges" => range_pairs(info[:used_window_ranges]), + "skipped_window_ranges" => range_pairs(info[:skipped_window_ranges]), + "accumulation" => haskey(info, :accumulation) ? String(info[:accumulation]) : nothing, + ) +end + +function source_summary(src::PointSource2D) + return Dict( + "kind" => "point", + "depth_m" => src.depth, + "lateral_m" => src.lateral, + "frequency_hz" => src.frequency, + "amplitude_pa" => src.amplitude, + "phase_rad" => src.phase, + "delay_s" => src.delay, + "num_cycles" => src.num_cycles, + ) +end + +function source_summary(src::BubbleCluster2D) + return Dict( + "kind" => "bubble_cluster", + "depth_m" => src.depth, + "lateral_m" => src.lateral, + "fundamental_hz" => src.fundamental, + "amplitude_pa" => src.amplitude, + "harmonics" => src.harmonics, + "harmonic_amplitudes" => src.harmonic_amplitudes, + "harmonic_phases_rad" => src.harmonic_phases, + "gate_duration_s" => src.gate_duration, + "delay_s" => src.delay, + ) +end + +function source_summary(src::PointSource3D) + return Dict( + "kind" => "point3d", + "depth_m" => src.depth, + "lateral_y_m" => src.lateral_y, + "lateral_z_m" => src.lateral_z, + "frequency_hz" => src.frequency, + "amplitude_pa" => src.amplitude, + "phase_rad" => src.phase, + "delay_s" => src.delay, + "num_cycles" => src.num_cycles, + ) +end + +function source_summary(src::BubbleCluster3D) + return Dict( + "kind" => "bubble3d", + "depth_m" => src.depth, + "lateral_y_m" => src.lateral_y, + "lateral_z_m" => src.lateral_z, + "fundamental_hz" => src.fundamental, + "amplitude_pa" => src.amplitude, + "harmonics" => src.harmonics, + "harmonic_amplitudes" => src.harmonic_amplitudes, + "harmonic_phases_rad" => src.harmonic_phases, + "gate_duration_s" => src.gate_duration, + "delay_s" => src.delay, + ) +end diff --git a/src/pam/sweep.jl b/src/pam/sweep.jl deleted file mode 100644 index 202ac6c..0000000 --- a/src/pam/sweep.jl +++ /dev/null @@ -1,250 +0,0 @@ -function _pam_mm_key(depth_mm::Real, lateral_mm::Real) - return (round(Float64(depth_mm); digits=6), round(Float64(lateral_mm); digits=6)) -end - -function _pam_mm_key(src::PointSource2D) - return _pam_mm_key(src.depth * 1e3, src.lateral * 1e3) -end - -function _resolve_pam_sweep_targets( - preset::Union{Symbol, AbstractString}; - axial_targets_mm::Union{Nothing, AbstractVector{<:Real}}=nothing, - lateral_targets_mm::Union{Nothing, AbstractVector{<:Real}}=nothing, -) - explicit_targets = !isnothing(axial_targets_mm) || !isnothing(lateral_targets_mm) - if explicit_targets - isnothing(axial_targets_mm) && error("Custom PAM sweep requires explicit axial target positions.") - isnothing(lateral_targets_mm) && error("Custom PAM sweep requires explicit lateral target positions.") - axial = sort(unique(Float64.(axial_targets_mm))) - lateral = sort(unique(Float64.(lateral_targets_mm))) - isempty(axial) && error("At least one axial target is required for a PAM sweep.") - isempty(lateral) && error("At least one lateral target is required for a PAM sweep.") - return :custom, axial, lateral - end - - mode = preset isa Symbol ? preset : Symbol(lowercase(strip(preset))) - if mode == :paper - return :paper, [30.0, 40.0, 50.0, 60.0, 70.0, 80.0], [-20.0, -10.0, 0.0, 10.0, 20.0] - elseif mode == :quick - return :quick, [40.0, 60.0, 80.0], [-10.0, 0.0, 10.0] - elseif mode == :custom - error("Custom PAM sweep requires both --axial-targets-mm and --lateral-targets-mm.") - end - error("Unknown PAM sweep preset: $preset") -end - -function _default_pam_sweep_examples(targets::AbstractVector{PointSource2D}) - isempty(targets) && error("At least one target is required to choose PAM sweep examples.") - - depth_values = sort(unique(Float64[src.depth * 1e3 for src in targets])) - num_examples = min(3, length(depth_values)) - selected_depth_indices = unique(round.(Int, collect(range(1, length(depth_values); length=num_examples)))) - - examples = Tuple{Float64, Float64}[] - for depth_idx in selected_depth_indices - depth_mm = depth_values[depth_idx] - candidates = [src for src in targets if isapprox(src.depth * 1e3, depth_mm; atol=1e-6)] - isempty(candidates) && continue - best = candidates[argmin(abs.([src.lateral for src in candidates]))] - push!(examples, _pam_mm_key(best)) - end - return examples -end - -function _normalize_pam_sweep_examples( - targets::AbstractVector{PointSource2D}, - example_targets_mm::Union{Nothing, AbstractVector{<:Tuple{<:Real, <:Real}}}, -) - if isnothing(example_targets_mm) - return _default_pam_sweep_examples(targets) - end - - 1 <= length(example_targets_mm) <= 3 || error("Provide between 1 and 3 PAM sweep example targets.") - available = Set(_pam_mm_key(src) for src in targets) - examples = Tuple{Float64, Float64}[] - for target in example_targets_mm - key = _pam_mm_key(target[1], target[2]) - key in available || error("Example target $(target[1]) mm, $(target[2]) mm is not part of the PAM sweep.") - push!(examples, key) - end - return sort(unique(examples)) -end - -function _pam_skull_cavity_start_rows( - c::AbstractMatrix{<:Real}; - c_water::Real=1500.0, - tol::Real=5.0, - min_thick_rows::Integer=2, -) - skull_mask = skull_mask_from_c_columnwise( - c; - c_water=c_water, - tol=tol, - min_thick_rows=min_thick_rows, - dilate_rows=1, - close_iters=1, - mask_outside=false, - ) - - ny = size(c, 2) - start_rows = zeros(Int, ny) - has_skull = falses(ny) - for col in 1:ny - rows = findall(skull_mask[:, col]) - isempty(rows) && continue - has_skull[col] = true - start_rows[col] = last(rows) + 1 - end - return start_rows, has_skull -end - -function _filter_pam_targets_in_skull_cavity( - c::AbstractMatrix{<:Real}, - cfg::PAMConfig, - targets::AbstractVector{PointSource2D}; - min_margin::Real=1e-3, - c_water::Real=cfg.c0, - tol::Real=5.0, - min_thick_rows::Integer=2, -) - kgrid = pam_grid(cfg) - cavity_start_rows, has_skull = _pam_skull_cavity_start_rows( - c; - c_water=c_water, - tol=tol, - min_thick_rows=min_thick_rows, - ) - margin_rows = max(0, ceil(Int, Float64(min_margin) / cfg.dx)) - - valid_targets = PointSource2D[] - dropped_targets = Dict{Symbol, Any}[] - - for src in targets - row, col = source_grid_index(src, cfg, kgrid) - truth_mm = (src.depth * 1e3, src.lateral * 1e3) - if !has_skull[col] - push!(dropped_targets, Dict{Symbol, Any}( - :truth_mm => truth_mm, - :row => row, - :col => col, - :reason => :no_skull_above, - )) - continue - end - - required_row = cavity_start_rows[col] + margin_rows - if row < required_row - push!(dropped_targets, Dict{Symbol, Any}( - :truth_mm => truth_mm, - :row => row, - :col => col, - :required_row => required_row, - :reason => :too_shallow_for_cavity, - )) - continue - end - - if abs(Float64(c[row, col]) - Float64(c_water)) > Float64(tol) - push!(dropped_targets, Dict{Symbol, Any}( - :truth_mm => truth_mm, - :row => row, - :col => col, - :reason => :non_fluid_target_cell, - )) - continue - end - - push!(valid_targets, src) - end - - return valid_targets, dropped_targets, cavity_start_rows -end - -function run_pam_sweep( - c::AbstractMatrix{<:Real}, - rho::AbstractMatrix{<:Real}, - targets::AbstractVector{PointSource2D}, - cfg::PAMConfig; - frequencies::Union{Nothing, AbstractVector{<:Real}}=nothing, - example_targets_mm::Union{Nothing, AbstractVector{<:Tuple{<:Real, <:Real}}}=nothing, - use_gpu::Bool=false, - runner::Function=run_pam_case, - case_callback::Union{Nothing, Function}=nothing, -) - isempty(targets) && error("At least one PAM sweep target is required.") - - sorted_targets = sort(collect(targets); by=src -> _pam_mm_key(src)) - axial_targets_mm = sort(unique(Float64[src.depth * 1e3 for src in sorted_targets])) - lateral_targets_mm = sort(unique(Float64[src.lateral * 1e3 for src in sorted_targets])) - axial_index = Dict(_pam_mm_key(depth_mm, 0.0)[1] => idx for (idx, depth_mm) in pairs(axial_targets_mm)) - lateral_index = Dict(_pam_mm_key(0.0, lateral_mm)[2] => idx for (idx, lateral_mm) in pairs(lateral_targets_mm)) - - geo_error_mm = fill(NaN, length(axial_targets_mm), length(lateral_targets_mm)) - hasa_error_mm = similar(geo_error_mm) - geo_peak_intensity = similar(geo_error_mm) - hasa_peak_intensity = similar(geo_error_mm) - - example_keys = Set(_normalize_pam_sweep_examples(sorted_targets, example_targets_mm)) - cases = Dict{Symbol, Any}[] - example_cases = Dict{Symbol, Any}[] - - for src in sorted_targets - results = runner( - c, - rho, - PointSource2D[src], - cfg; - frequencies=frequencies, - use_gpu=use_gpu, - ) - stats_geo = results[:stats_geo] - stats_hasa = results[:stats_hasa] - - target_key = _pam_mm_key(src) - row = axial_index[target_key[1]] - col = lateral_index[target_key[2]] - geo_error_mm[row, col] = Float64(stats_geo[:mean_radial_error_mm]) - hasa_error_mm[row, col] = Float64(stats_hasa[:mean_radial_error_mm]) - geo_peak_intensity[row, col] = Float64(stats_geo[:mean_norm_peak_intensity]) - hasa_peak_intensity[row, col] = Float64(stats_hasa[:mean_norm_peak_intensity]) - - case_result = Dict{Symbol, Any}( - :source => src, - :truth_mm => (src.depth * 1e3, src.lateral * 1e3), - :stats_geo => stats_geo, - :stats_hasa => stats_hasa, - :geo_predicted_mm => only(stats_geo[:predicted_mm]), - :hasa_predicted_mm => only(stats_hasa[:predicted_mm]), - :reconstruction_frequencies => results[:reconstruction_frequencies], - :simulation => results[:simulation], - ) - push!(cases, case_result) - - if !isnothing(case_callback) - case_callback(case_result, results) - end - - if target_key in example_keys - example_result = copy(case_result) - example_result[:rf] = results[:rf] - example_result[:pam_geo] = results[:pam_geo] - example_result[:pam_hasa] = results[:pam_hasa] - example_result[:kgrid] = results[:kgrid] - push!(example_cases, example_result) - end - end - - sort!(example_cases; by=case -> _pam_mm_key(case[:truth_mm]...)) - return Dict{Symbol, Any}( - :cases => cases, - :axial_targets_mm => axial_targets_mm, - :lateral_targets_mm => lateral_targets_mm, - :geo_error_mm => geo_error_mm, - :hasa_error_mm => hasa_error_mm, - :geo_peak_intensity => geo_peak_intensity, - :hasa_peak_intensity => hasa_peak_intensity, - :example_cases => example_cases, - :example_targets_mm => [case[:truth_mm] for case in example_cases], - ) -end - diff --git a/test/common/kwave_wrapper.jl b/test/common/kwave_wrapper.jl new file mode 100644 index 0000000..8524ce4 --- /dev/null +++ b/test/common/kwave_wrapper.jl @@ -0,0 +1,103 @@ +@testset "k-Wave wrapper helpers" begin + @test TranscranialFUS._normalize_record(:p_rms) == :p_rms + @test TranscranialFUS._normalize_record("p") == :p + @test_throws ErrorException TranscranialFUS._normalize_record(:pressure) + + mat = reshape(1:6, 2, 3) + @test TranscranialFUS._as_sensor_matrix(mat, 2, 3) == Float64.(mat) + @test TranscranialFUS._as_sensor_matrix(permutedims(mat), 2, 3) == Float64.(mat) + @test TranscranialFUS._as_sensor_matrix([1, 2, 3], 3, 1) == reshape(Float64[1, 2, 3], 3, 1) + @test_throws ErrorException TranscranialFUS._as_sensor_matrix(zeros(2, 2, 2), 2, 4) +end + +@testset "k-Wave deterministic planning helpers" begin + cfg = PAMConfig( + dx=1e-3, + dz=1e-3, + axial_dim=0.012, + transverse_dim=0.008, + t_max=4e-6, + dt=0.1e-6, + PML_GUARD=2, + ) + kgrid = pam_grid(cfg) + c = fill(Float32(cfg.c0), pam_Nx(cfg), pam_Ny(cfg)) + rho = fill(Float32(cfg.rho0), size(c)) + source_a = PointSource2D(depth=0.003, lateral=0.0, frequency=0.4e6) + source_b = PointSource2D(depth=0.003, lateral=0.0, frequency=0.6e6) + + @test TranscranialFUS._validate_point_source_inputs(c, rho, [source_a], cfg) == size(c) + indexed = TranscranialFUS._indexed_sources_2d([source_b, source_a], cfg, kgrid, pam_Nx(cfg)) + grouped = TranscranialFUS._group_sources_2d(indexed) + @test length(grouped) == 1 + @test length(last(only(grouped))) == 2 + @test sort(TranscranialFUS._unique_emission_frequencies(indexed)) == [0.4e6, 0.6e6] + info = TranscranialFUS._kwave_info_2d(receiver_row(cfg), receiver_col_range(cfg), [first(only(grouped))], [source_a, source_b], grouped) + @test info[:num_input_sources] == 2 + @test info[:num_source_points] == 1 + + cfg3 = PAMConfig3D( + dx=1e-3, + dy=1e-3, + dz=1e-3, + axial_dim=0.012, + transverse_dim_y=0.006, + transverse_dim_z=0.006, + t_max=4e-6, + dt=0.1e-6, + PML_GUARD=2, + ) + c3 = fill(Float32(cfg3.c0), pam_Nx(cfg3), pam_Ny(cfg3), pam_Nz(cfg3)) + rho3 = fill(Float32(cfg3.rho0), size(c3)) + source3a = PointSource3D(depth=0.003, frequency=0.5e6) + source3b = PointSource3D(depth=0.003, frequency=0.75e6) + + @test TranscranialFUS._validate_point_source_inputs_3d(c3, rho3, [source3a], cfg3) == size(c3) + indexed3 = TranscranialFUS._indexed_sources_3d([source3b, source3a], cfg3, pam_Nx(cfg3), pam_Ny(cfg3)) + grouped3 = TranscranialFUS._group_sources_3d(indexed3) + @test length(grouped3) == 1 + @test length(last(only(grouped3))) == 2 + @test sort(TranscranialFUS._unique_emission_frequencies(indexed3)) == [0.5e6, 0.75e6] + info3 = TranscranialFUS._kwave_info_3d(receiver_row(cfg3), receiver_col_range_y(cfg3), receiver_col_range_z(cfg3), [first(only(grouped3))], [source3a, source3b], grouped3) + @test info3[:receiver_cols_y] == receiver_col_range_y(cfg3) + @test info3[:num_input_sources] == 2 + @test info3[:num_source_points] == 1 +end + +@testset "PAM k-Wave validation paths" begin + cfg = PAMConfig( + dx=1e-3, + dz=1e-3, + axial_dim=0.01, + transverse_dim=0.008, + t_max=4e-6, + dt=0.1e-6, + PML_GUARD=2, + ) + source = PointSource2D(depth=0.003, lateral=0.0) + c = fill(Float32(cfg.c0), pam_Nx(cfg), pam_Ny(cfg)) + rho = fill(Float32(cfg.rho0), size(c)) + + @test_throws ErrorException simulate_point_sources(c, rho, PointSource2D[], cfg) + @test_throws ErrorException simulate_point_sources(c, rho[:, 1:(end - 1)], [source], cfg) + @test_throws ErrorException simulate_point_sources(c[1:(end - 1), :], rho[1:(end - 1), :], [source], cfg) + + cfg3 = PAMConfig3D( + dx=1e-3, + dy=1e-3, + dz=1e-3, + axial_dim=0.01, + transverse_dim_y=0.006, + transverse_dim_z=0.006, + t_max=4e-6, + dt=0.1e-6, + PML_GUARD=2, + ) + source3 = PointSource3D(depth=0.003) + c3 = fill(Float32(cfg3.c0), pam_Nx(cfg3), pam_Ny(cfg3), pam_Nz(cfg3)) + rho3 = fill(Float32(cfg3.rho0), size(c3)) + + @test_throws ErrorException simulate_point_sources_3d(c3, rho3, PointSource3D[], cfg3) + @test_throws ErrorException simulate_point_sources_3d(c3, rho3[:, 1:(end - 1), :], [source3], cfg3) + @test_throws ErrorException simulate_point_sources_3d(c3[1:(end - 1), :, :], rho3[1:(end - 1), :, :], [source3], cfg3) +end diff --git a/test/pam/2d/analysis.jl b/test/pam/2d/analysis.jl new file mode 100644 index 0000000..80218a0 --- /dev/null +++ b/test/pam/2d/analysis.jl @@ -0,0 +1,124 @@ +@testset "localization metrics" begin + cfg = PAMConfig( + dx=0.5e-3, + dz=0.5e-3, + axial_dim=0.05, + transverse_dim=0.04, + receiver_aperture=nothing, + PML_GUARD=5, + peak_suppression_radius=2e-3, + success_tolerance=1.5e-3, + ) + kgrid = pam_grid(cfg) + depth = depth_coordinates(kgrid, cfg) + lateral = kgrid.y_vec + sources = [ + PointSource2D(depth=0.015, lateral=-0.004, frequency=0.4e6), + PointSource2D(depth=0.028, lateral=0.006, frequency=0.4e6), + ] + + intensity = zeros(Float64, kgrid.Nx, kgrid.Ny) + σd = 0.8e-3 + σl = 0.8e-3 + for src in sources + for i in 1:kgrid.Nx, j in 1:kgrid.Ny + intensity[i, j] += exp(-((depth[i] - src.depth)^2 / (2σd^2) + (lateral[j] - src.lateral)^2 / (2σl^2))) + end + end + + stats = analyse_pam_2d(intensity, kgrid, cfg, sources) + @test stats[:mean_radial_error_mm] < 0.6 + @test stats[:num_success] == 2 + @test length(stats[:axial_fwhm_mm]) == 2 + @test all(stats[:axial_fwhm_mm] .> 0) + @test all(stats[:lateral_fwhm_mm] .> 0) + + metrics = pam_intensity_metrics(intensity, kgrid, cfg; threshold_ratio=0.5, reference_intensity=2.0) + @test metrics[:peak_intensity] ≈ maximum(intensity) + @test metrics[:relative_peak_intensity] ≈ maximum(intensity) / 2.0 + @test metrics[:integrated_intensity_m2] > 0 + @test metrics[:active_area_mm2] > 0 + @test isfinite(metrics[:centroid_depth_mm]) + @test isfinite(metrics[:centroid_lateral_mm]) +end + +@testset "detection metrics" begin + cfg = PAMConfig( + dx=0.5e-3, + dz=0.5e-3, + axial_dim=0.05, + transverse_dim=0.04, + receiver_aperture=nothing, + PML_GUARD=5, + success_tolerance=1.0e-3, + ) + kgrid = pam_grid(cfg) + sources = [ + PointSource2D(depth=0.015, lateral=-0.004, frequency=0.4e6), + PointSource2D(depth=0.028, lateral=0.006, frequency=0.4e6), + ] + intensity = zeros(Float64, kgrid.Nx, kgrid.Ny) + row1, col1 = source_grid_index(sources[1], cfg, kgrid) + row_false, col_false = source_grid_index(PointSource2D(depth=0.038, lateral=-0.012), cfg, kgrid) + intensity[row1, col1] = 1.0 + intensity[row_false, col_false] = 0.8 + + stats = analyse_pam_detection_2d( + intensity, + kgrid, + cfg, + sources; + truth_radius=1.0e-3, + threshold_ratio=0.5, + frequencies=[0.4e6], + psf_axial_fwhm=2.0e-3, + psf_lateral_fwhm=2.0e-3, + ) + + @test 0 < stats[:precision] < 1 + @test 0 < stats[:recall] < 1 + @test stats[:false_positive_pixels] > 0 + @test stats[:false_negative_pixels] > 0 + @test stats[:spurious_prediction_components] == 1 + @test 0 < stats[:energy_fraction_inside_mask] < 1 + @test stats[:energy_fraction_inside_mask] + stats[:energy_fraction_outside_mask] ≈ 1.0 + @test stats[:energy_fraction_inside_predicted_mask] > stats[:energy_fraction_inside_mask] + @test stats[:energy_fraction_inside_predicted_mask] + stats[:energy_fraction_outside_predicted_mask] ≈ 1.0 + @test isfinite(stats[:centroid_error_mm]) + @test stats[:axial_spread_mm] > 0 + @test stats[:lateral_spread_mm] > 0 + @test haskey(stats, :psf_target_correlation) + @test isfinite(stats[:psf_target_normalized_l2_error]) + + truth_override = falses(kgrid.Nx, kgrid.Ny) + truth_override[row_false, col_false] = true + override_stats = analyse_pam_detection_2d( + intensity, + kgrid, + cfg, + sources; + truth_radius=1.0e-3, + threshold_ratio=0.5, + truth_mask=truth_override, + psf_axial_fwhm=2.0e-3, + psf_lateral_fwhm=2.0e-3, + ) + @test override_stats[:truth_mask_mode] == :provided + @test override_stats[:psf_target_mode] == :provided_mask + @test override_stats[:true_positive_pixels] == 1 + @test override_stats[:false_positive_pixels] == 1 + @test override_stats[:false_negative_pixels] == 0 + + source_map = pam_source_map(sources, kgrid, cfg; weights=:uniform) + @test sum(source_map) == length(sources) + blurred_truth = pam_psf_blurred_truth_map( + sources, + kgrid, + cfg; + psf_axial_fwhm=2.0e-3, + psf_lateral_fwhm=2.0e-3, + weights=:uniform, + ) + @test size(blurred_truth) == size(intensity) + @test sum(blurred_truth) ≈ sum(source_map) atol=1e-8 +end diff --git a/test/pam/2d/config.jl b/test/pam/2d/config.jl new file mode 100644 index 0000000..09f9401 --- /dev/null +++ b/test/pam/2d/config.jl @@ -0,0 +1,67 @@ +@testset "config" begin + cfg_fine = PAMConfig(dx=0.2e-3, dz=0.2e-3, axial_dim=0.03, transverse_dim=0.03) + cfg_coarse = PAMConfig(dx=0.5e-3, dz=0.5e-3, axial_dim=0.03, transverse_dim=0.03) + + @test TranscranialFUS._pam_pml_guard(cfg_fine) == 20 + @test TranscranialFUS._pam_pml_guard(cfg_coarse) == 8 + @test receiver_row(cfg_coarse) == 1 + + kgrid = pam_grid(cfg_coarse) + src = PointSource2D(depth=0.015, lateral=0.003, frequency=0.4e6) + row, _ = source_grid_index(src, cfg_coarse, kgrid) + @test row == 31 + @test row <= pam_Nx(cfg_coarse) + + base_cfg = PAMConfig(dx=1e-3, dz=1e-3, axial_dim=0.03, transverse_dim=0.06) + fitted_cfg = fit_pam_config( + base_cfg, + [PointSource2D(depth=0.04, lateral=0.0)]; + min_bottom_margin=5e-3, + reference_depth=30e-3, + ) + @test pam_Nx(fitted_cfg) == 46 + @test fitted_cfg.axial_dim ≈ 46e-3 + + deep_cfg = PAMConfig( + dx=1e-3, + dz=1e-3, + axial_dim=0.08, + transverse_dim=0.06, + receiver_aperture=0.04, + t_max=80e-6, + dt=50e-9, + ) + cluster = BubbleCluster2D( + depth=0.08, + lateral=0.0, + fundamental=0.5e6, + gate_duration=50e-6, + ) + fitted_deep = fit_pam_config(deep_cfg, [cluster]; min_bottom_margin=5e-3) + @test fitted_deep.t_max >= TranscranialFUS._required_pam_t_max(deep_cfg, [cluster]) + @test fitted_deep.t_max > 110e-6 + @test TranscranialFUS._pam_axial_substeps(0.2e-3, 50e-6) == 4 + + @test TranscranialFUS.pam_reconstruction_mode(:auto, :squiggle) == :windowed + @test TranscranialFUS.pam_reconstruction_mode(:auto, :point) == :full + @test TranscranialFUS.pam_reconstruction_mode(:full, :squiggle) == :full + @test_throws ErrorException TranscranialFUS.pam_reconstruction_mode(:auto, :unknown) +end + +@testset "windowing helpers" begin + cfg = PAMWindowConfig(enabled=true, window_duration=10e-6, hop=5e-6) + exact_ranges, exact_win, exact_hop = TranscranialFUS._pam_window_ranges(100, 0.1e-6, cfg) + @test exact_ranges == [1:100] + @test exact_win == 100 + @test exact_hop == 50 + + overlap_ranges, overlap_win, overlap_hop = TranscranialFUS._pam_window_ranges(250, 0.1e-6, cfg) + @test overlap_win == 100 + @test overlap_hop == 50 + @test overlap_ranges == [1:100, 51:150, 101:200, 151:250] + + short_ranges, short_win, short_hop = TranscranialFUS._pam_window_ranges(40, 0.1e-6, cfg) + @test short_ranges == [1:40] + @test short_win == 40 + @test short_hop == 50 +end diff --git a/test/pam/2d/medium.jl b/test/pam/2d/medium.jl new file mode 100644 index 0000000..f0150de --- /dev/null +++ b/test/pam/2d/medium.jl @@ -0,0 +1,43 @@ +@testset "medium" begin + cfg = PAMConfig(dx=1e-3, dz=1e-3, axial_dim=0.04, transverse_dim=0.06) + c_water, rho_water, info_water = make_pam_medium(cfg; aberrator=:none) + @test size(c_water) == (pam_Nx(cfg), pam_Ny(cfg)) + @test size(rho_water) == size(c_water) + @test all(c_water .≈ cfg.c0) + @test all(rho_water .≈ cfg.rho0) + @test info_water[:aberrator] == :none + + targets = [ + PointSource2D(depth=axial_mm * 1e-3, lateral=0.0, frequency=1e6) + for axial_mm in (40.0, 60.0, 80.0) + ] + fitted_cfg = fit_pam_config( + cfg, + targets; + min_bottom_margin=5e-3, + reference_depth=30e-3, + ) + hu_vol = synthetic_hu_volume() + c, rho, info = make_pam_medium( + fitted_cfg; + aberrator=:skull, + hu_vol=hu_vol, + spacing_m=(1e-3, 1e-3, 1e-3), + slice_index=2, + skull_to_transducer=30e-3, + hu_bone_thr=200, + ) + + @test size(c) == (pam_Nx(fitted_cfg), pam_Ny(fitted_cfg)) + @test size(rho) == size(c) + @test info[:outer_row] == receiver_row(fitted_cfg) + 30 + @test info[:outer_row] < info[:inner_row] + @test maximum(c[info[:outer_row]:info[:inner_row], :]) > fitted_cfg.c0 + + for src in targets + row, col = source_grid_index(src, fitted_cfg, pam_grid(fitted_cfg)) + @test row > info[:inner_row] + @test row <= pam_Nx(fitted_cfg) + @test c[row, col] ≈ fitted_cfg.c0 + end +end diff --git a/test/pam/2d/plots.jl b/test/pam/2d/plots.jl new file mode 100644 index 0000000..63111b1 --- /dev/null +++ b/test/pam/2d/plots.jl @@ -0,0 +1,75 @@ +using CairoMakie + +@testset "plot data helpers" begin + map = [0.0 1.0; 10.0 100.0] + + @test TranscranialFUS.map_norm(map, 100.0) ≈ [0.0 0.01; 0.1 1.0] + @test TranscranialFUS.map_norm(map, 0.0)[2, 2] ≈ 100.0 / eps(Float64) + db = TranscranialFUS.map_db(map, 100.0) + @test db[2, 2] ≈ 0.0 + @test db[1, 1] < -100 + + sources = [ + PointSource2D(depth=0.01, lateral=-0.002), + PointSource2D(depth=0.02, lateral=0.003), + ] + pairs = TranscranialFUS.source_pairs_mm(sources) + @test pairs == [(10.0, -2.0), (20.0, 3.0)] + + stats = Dict(:mean_radial_error_mm => 0.25, :num_success => 1, :num_truth_sources => 2, :mean_norm_peak_intensity => 0.75) + line = TranscranialFUS.summary_line(stats) + @test occursin("err=0.25 mm", line) + @test occursin("success=1/2", line) + + detection_line = TranscranialFUS.summary_line(Dict(:f1 => 0.5, :precision => 0.25, :recall => 1.0)) + @test occursin("F1=0.5", detection_line) + @test occursin("recall=1.0", detection_line) + @test TranscranialFUS.summary_line(Dict(:other => 1)) == "Dict(:other => 1)" +end + +@testset "plot rendering helpers" begin + fig = CairoMakie.Figure(size=(300, 240)) + ax = CairoMakie.Axis(fig[1, 1]) + c_water = fill(1500.0, 6, 6) + @test TranscranialFUS.overlay_skull_2d!(ax, c_water, 1:6, 1:6) === nothing + @test TranscranialFUS.lines_centerlines!(ax, nothing) === nothing + @test TranscranialFUS.lines_centerlines!(ax, [[(0.001, 0.0)]]) === nothing + @test TranscranialFUS.lines_centerlines!(ax, [[(0.001, -0.001), (0.002, 0.001)]]) === nothing + + cfg = PAMConfig( + dx=1e-3, + dz=1e-3, + axial_dim=0.008, + transverse_dim=0.008, + peak_suppression_radius=1e-3, + success_tolerance=1e-3, + PML_GUARD=1, + ) + kgrid = pam_grid(cfg) + source = PointSource2D(depth=0.003, lateral=0.0) + intensity = zeros(Float64, pam_Nx(cfg), pam_Ny(cfg)) + intensity[source_grid_index(source, cfg, kgrid)...] = 1.0 + truth_mask = pam_truth_mask([source], kgrid, cfg; radius=cfg.success_tolerance) + + mktempdir() do dir + path = joinpath(dir, "boundary.png") + metrics = TranscranialFUS.save_threshold_boundary_detection( + path, + intensity, + 0.8 .* intensity, + kgrid, + cfg, + [source]; + threshold_ratios=[0.5, 0.9], + truth_radius=cfg.success_tolerance, + truth_mask=truth_mask, + truth_centerlines=[[(0.002, -0.001), (0.004, 0.001)]], + frequencies=[source.frequency], + c=c_water, + ) + @test isfile(path) + @test metrics["threshold_ratios"] == [0.5, 0.9] + @test length(metrics["geometric"]) == 2 + @test haskey(first(metrics["hasa"]), "f1") + end +end diff --git a/test/pam/2d/reconstruction.jl b/test/pam/2d/reconstruction.jl new file mode 100644 index 0000000..a77cdce --- /dev/null +++ b/test/pam/2d/reconstruction.jl @@ -0,0 +1,204 @@ +@testset "reconstruction in water" begin + cfg = PAMConfig( + dx=0.5e-3, + dz=0.5e-3, + axial_dim=0.04, + transverse_dim=0.03, + receiver_aperture=nothing, + t_max=40e-6, + dt=50e-9, + PML_GUARD=5, + zero_pad_factor=2, + peak_suppression_radius=1e-3, + success_tolerance=1.0e-3, + ) + source = PointSource2D(depth=0.015, lateral=0.003, frequency=0.4e6, amplitude=1.0, num_cycles=5) + c, _, _ = make_pam_medium(cfg; aberrator=:none) + rf, kgrid = analytic_rf_for_point_source(cfg, source) + cuda_ok = TranscranialFUS._pam_cuda_functional() + + quiet_result, quiet_progress = capture_stderr_result() do + reconstruct_pam(rf, c, cfg; frequencies=[source.frequency], corrected=false) + end + intensity, _, info = quiet_result + @test quiet_progress == "" + stats = analyse_pam_2d(intensity, kgrid, cfg, [source]) + + hasa_result, hasa_progress = capture_stderr_result() do + reconstruct_pam( + rf, + c, + cfg; + frequencies=[source.frequency], + corrected=true, + use_gpu=cuda_ok, + show_progress=true, + ) + end + intensity_hasa, _, info_hasa = hasa_result + stats_hasa = analyse_pam_2d(intensity_hasa, kgrid, cfg, [source]) + + one_window = PAMWindowConfig( + enabled=true, + window_duration=pam_Nt(cfg) * cfg.dt, + hop=pam_Nt(cfg) * cfg.dt, + taper=:none, + min_energy_ratio=0.0, + ) + windowed_result, windowed_progress = capture_stderr_result() do + reconstruct_pam_windowed( + rf, + c, + cfg; + frequencies=[source.frequency], + corrected=false, + window_config=one_window, + use_gpu=cuda_ok, + show_progress=true, + ) + end + intensity_windowed, _, info_windowed = windowed_result + + cropped_range = 101:500 + cropped_origin = (first(cropped_range) - 1) * cfg.dt + intensity_cropped, _, info_cropped = reconstruct_pam( + rf[:, cropped_range], + c, + cfg; + frequencies=[source.frequency], + use_gpu=cuda_ok, + corrected=false, + time_origin=cropped_origin, + ) + stats_cropped = analyse_pam_2d(intensity_cropped, kgrid, cfg, [source]) + + @test info[:corrected] == false + @test info[:show_progress] == false + @test stats[:mean_radial_error_mm] < 1.0 + @test stats[:success_rate] == 1.0 + @test stats[:mean_norm_peak_intensity] > 0.5 + @test info_hasa[:corrected] == true + @test info_hasa[:use_gpu] == cuda_ok + @test info_hasa[:backend] == (cuda_ok ? :cuda : :cpu) + @test info_hasa[:gpu_precision] == (cuda_ok ? Float32 : nothing) + @test info_hasa[:show_progress] == true + @test occursin("PAM HASA reconstruction", hasa_progress) + if cuda_ok + @test occursin("freq batch march elapsed", hasa_progress) + else + @test occursin("frequency 1/", hasa_progress) + end + @test occursin("total elapsed", hasa_progress) + @test stats_hasa[:mean_radial_error_mm] < 1.0 + @test stats_hasa[:success_rate] == 1.0 + + if cuda_ok + @test pam_gpu_maps_approx(intensity_windowed, intensity) + intensity_hasa_cpu, _, _ = reconstruct_pam( + rf, + c, + cfg; + frequencies=[source.frequency], + corrected=true, + use_gpu=false, + ) + @test pam_gpu_maps_approx(intensity_hasa, intensity_hasa_cpu) + + c_lens = copy(c) + c_lens[20:30, 25:35] .= 1700.0 + intensity_lens_cpu, _, _ = reconstruct_pam( + rf, + c_lens, + cfg; + frequencies=[source.frequency], + corrected=true, + use_gpu=false, + ) + intensity_lens_gpu, _, info_lens_gpu = reconstruct_pam( + rf, + c_lens, + cfg; + frequencies=[source.frequency], + corrected=true, + use_gpu=true, + ) + @test info_lens_gpu[:backend] == :cuda + @test pam_gpu_maps_approx(intensity_lens_gpu, intensity_lens_cpu) + else + @test intensity_windowed ≈ intensity + err = try + reconstruct_pam(rf, c, cfg; frequencies=[source.frequency], corrected=false, use_gpu=true) + nothing + catch err + err + end + @test err isa ErrorException + @test occursin("functional NVIDIA CUDA GPU", sprint(showerror, err)) + end + + @test info_windowed[:use_gpu] == cuda_ok + @test info_windowed[:backend] == (cuda_ok ? :cuda : :cpu) + @test info_windowed[:show_progress] == true + @test occursin("PAM geometric ASA windowed reconstruction", windowed_progress) + @test occursin("window 1/1", windowed_progress) + @test occursin("complete", windowed_progress) + @test info_windowed[:used_window_count] == 1 + @test info_windowed[:skipped_window_count] == 0 + @test info_cropped[:time_origin] ≈ cropped_origin + @test info_cropped[:use_gpu] == cuda_ok + @test info_cropped[:backend] == (cuda_ok ? :cuda : :cpu) + @test stats_cropped[:success_rate] == 1.0 +end + +@testset "spectral helpers" begin + p1 = fill(1.0 + 0.0im, 2, 2) + p2 = fill(-1.0 + 0.0im, 2, 2) + @test all(abs2.(p1 .+ p2) .== 0.0) + @test all(abs2.(p1) .+ abs2.(p2) .== 2.0) + + @test TranscranialFUS._format_elapsed(5e-7) == "0.5 us" + @test TranscranialFUS._format_elapsed(5e-3) == "5.0 ms" + @test TranscranialFUS._format_elapsed(5.0) == "5.0 s" + @test TranscranialFUS._format_elapsed(65.0) == "1m 5.0s" + @test TranscranialFUS._format_frequency_list(Float64[]) == "none" + @test occursin("...", TranscranialFUS._format_frequency_list(collect(1.0:10.0) .* 1e6; max_items=4)) +end + +@testset "windowed reconstruction skips empty windows" begin + cfg = PAMConfig( + dx=1e-3, + dz=1e-3, + axial_dim=0.01, + transverse_dim=0.008, + t_max=8e-6, + dt=0.1e-6, + zero_pad_factor=1, + PML_GUARD=2, + ) + c = fill(Float64(cfg.c0), pam_Nx(cfg), pam_Ny(cfg)) + rf = zeros(Float64, pam_Ny(cfg), pam_Nt(cfg)) + window_config = PAMWindowConfig( + enabled=true, + window_duration=2e-6, + hop=1e-6, + taper=:hann, + min_energy_ratio=0.5, + ) + + intensity, grid, info = reconstruct_pam_windowed( + rf, + c, + cfg; + frequencies=[0.5e6], + corrected=false, + window_config=window_config, + use_gpu=false, + ) + + @test all(iszero, intensity) + @test grid.Nt == pam_Nt(cfg) + @test info[:used_window_count] == 0 + @test info[:skipped_window_count] == info[:total_window_count] + @test info[:energy_threshold] == 0.0 + @test info[:backend] == :cpu +end diff --git a/test/pam/2d/sources.jl b/test/pam/2d/sources.jl new file mode 100644 index 0000000..f9bf3a7 --- /dev/null +++ b/test/pam/2d/sources.jl @@ -0,0 +1,104 @@ +@testset "bubble cluster emissions" begin + dt = 20e-9 + nt = 1000 + src = BubbleCluster2D( + depth=0.03, + lateral=0.0, + fundamental=0.5e6, + amplitude=1.0, + harmonics=[2], + harmonic_amplitudes=[1.0], + harmonic_phases=[0.0], + gate_duration=10e-6, + ) + signal = TranscranialFUS._source_signal(nt, dt, src) + active = findall(!iszero, signal) + + @test !isempty(active) + @test maximum(abs, signal) > 0.5 + @test abs(signal[first(active)]) < 0.05 * maximum(abs, signal) + @test abs(signal[last(active)]) < 0.05 * maximum(abs, signal) + @test emission_frequencies(src) == [1.0e6] + + spectrum = abs.(fft(signal)) + freq_axis = collect(0:(nt - 1)) ./ (nt * dt) + pos_bins = 2:(fld(nt, 2) + 1) + peak_bin = pos_bins[argmax(spectrum[pos_bins])] + @test abs(freq_axis[peak_bin] - 1.0e6) <= 1 / (nt * dt) +end + +@testset "source phase modes" begin + @test TranscranialFUS._normalize_source_phase_mode(:coherent) == :coherent + @test TranscranialFUS._normalize_source_phase_mode(:random_static_phase) == :random_static_phase + @test TranscranialFUS._normalize_source_phase_mode(:random_phase_per_window) == :random_phase_per_window + @test_throws ErrorException TranscranialFUS._normalize_source_phase_mode(:unknown_mode) + + @test TranscranialFUS._normalize_cluster_phase_mode(:random_static_phase) == :random + @test TranscranialFUS._normalize_cluster_phase_mode("random_static_phase") == :random + @test TranscranialFUS._normalize_cluster_phase_mode(:coherent) == :coherent + + sources_orig = [ + BubbleCluster2D(depth=0.03, lateral=0.0, fundamental=0.5e6, + harmonics=[2, 3], harmonic_amplitudes=[1.0, 0.6], + harmonic_phases=[0.1, 0.2], gate_duration=10e-6), + PointSource2D(depth=0.02, lateral=0.005, frequency=1.0e6, phase=0.5), + ] + resampled = TranscranialFUS._resample_source_phases(sources_orig, Random.MersenneTwister(7)) + + @test resampled[1].depth == sources_orig[1].depth + @test resampled[1].lateral == sources_orig[1].lateral + @test resampled[1].harmonic_phases != sources_orig[1].harmonic_phases + @test resampled[2].phase != sources_orig[2].phase +end + +@testset "source variability" begin + src = BubbleCluster2D(depth=0.03, lateral=0.0, fundamental=0.5e6, + harmonics=[2, 3], harmonic_amplitudes=[1.0, 0.6], + harmonic_phases=[0.1, 0.2], gate_duration=50e-6) + + expanded, n = TranscranialFUS._expand_sources_per_window( + [src], 10e-6, 5e-6, 80e-6, Random.MersenneTwister(1)) + @test n == 15 + @test length(expanded) == 15 + @test all(s.amplitude == src.amplitude for s in expanded) + @test all(s.fundamental == src.fundamental for s in expanded) + + exp_fj, _ = TranscranialFUS._expand_sources_per_window( + [src], 10e-6, 5e-6, 80e-6, Random.MersenneTwister(99); + variability=SourceVariabilityConfig(frequency_jitter_fraction=0.05)) + @test length(unique(round.(Float64[s.fundamental for s in exp_fj]; digits=0))) > 1 +end + +@testset "squiggle bubble sources" begin + squiggle_clusters, squiggle_meta = make_squiggle_bubble_sources( + [(0.03, 0.0)]; + root_length=12e-3, + squiggle_amplitude=1.5e-3, + squiggle_wavelength=6e-3, + source_spacing=1e-3, + position_jitter=0.0, + min_separation=0.0, + lateral_bounds=(-0.02, 0.02), + rng=Random.MersenneTwister(41), + ) + + @test squiggle_meta[:source_model] == :squiggle + @test all(src -> src isa BubbleCluster2D, squiggle_clusters) + @test length(squiggle_meta[:centerlines]) == 1 + @test maximum(src.lateral for src in squiggle_clusters) - minimum(src.lateral for src in squiggle_clusters) > 10e-3 + @test maximum(src.depth for src in squiggle_clusters) - minimum(src.depth for src in squiggle_clusters) > 2e-3 + + multi_clusters, multi_meta = make_squiggle_bubble_sources( + [(0.03, -0.004), (0.035, 0.004)]; + root_length=8e-3, + source_spacing=1e-3, + position_jitter=0.0, + min_separation=0.0, + max_sources_per_anchor=20, + lateral_bounds=(-0.02, 0.02), + rng=Random.MersenneTwister(43), + ) + @test length(multi_clusters) > length(squiggle_clusters) + @test multi_meta[:source_model] == :squiggle + @test length(multi_meta[:centerlines]) == 2 +end diff --git a/test/pam/2d/workflow.jl b/test/pam/2d/workflow.jl new file mode 100644 index 0000000..0c935c8 --- /dev/null +++ b/test/pam/2d/workflow.jl @@ -0,0 +1,103 @@ +@testset "workflow case assembly" begin + cfg = PAMConfig( + dx=0.5e-3, + dz=0.5e-3, + axial_dim=0.04, + transverse_dim=0.03, + receiver_aperture=nothing, + t_max=40e-6, + dt=50e-9, + PML_GUARD=5, + zero_pad_factor=2, + peak_suppression_radius=1e-3, + success_tolerance=1.0e-3, + ) + source = PointSource2D(depth=0.015, lateral=0.003, frequency=0.4e6, amplitude=1.0, num_cycles=5) + c, _, _ = make_pam_medium(cfg; aberrator=:none) + rf, kgrid = analytic_rf_for_point_source(cfg, source) + cuda_ok = TranscranialFUS._pam_cuda_functional() + + cached_results = reconstruct_pam_case( + rf, + c, + [source], + cfg; + simulation_info=Dict(:receiver_row => receiver_row(cfg), :receiver_cols => receiver_col_range(cfg)), + frequencies=[source.frequency], + use_gpu=cuda_ok, + ) + @test cached_results[:simulation][:receiver_row] == receiver_row(cfg) + @test cached_results[:use_gpu] == cuda_ok + @test cached_results[:show_progress] == false + @test cached_results[:geo_info][:use_gpu] == cuda_ok + @test cached_results[:hasa_info][:use_gpu] == cuda_ok + @test cached_results[:geo_info][:backend] == (cuda_ok ? :cuda : :cpu) + @test cached_results[:hasa_info][:backend] == (cuda_ok ? :cuda : :cpu) + @test cached_results[:stats_geo][:success_rate] == 1.0 + @test cached_results[:stats_hasa][:success_rate] == 1.0 + + one_window = PAMWindowConfig( + enabled=true, + window_duration=pam_Nt(cfg) * cfg.dt, + hop=pam_Nt(cfg) * cfg.dt, + taper=:none, + min_energy_ratio=0.0, + ) + windowed_results = reconstruct_pam_case( + rf, + c, + [source], + cfg; + simulation_info=Dict(:receiver_row => receiver_row(cfg), :receiver_cols => receiver_col_range(cfg)), + frequencies=[source.frequency], + reconstruction_mode=:windowed, + window_config=one_window, + ) + @test windowed_results[:reconstruction_mode] == :windowed + @test windowed_results[:geo_info][:used_window_count] == 1 + + duplicate_source_events = [source, PointSource2D(depth=source.depth, lateral=source.lateral, frequency=source.frequency)] + truth_mask = pam_truth_mask([source], kgrid, cfg; radius=cfg.success_tolerance) + event_results = reconstruct_pam_case( + rf, + c, + duplicate_source_events, + cfg; + simulation_info=Dict(:receiver_row => receiver_row(cfg), :receiver_cols => receiver_col_range(cfg)), + frequencies=[source.frequency], + analysis_mode=:detection, + detection_truth_mask=truth_mask, + analysis_sources=[source], + ) + @test event_results[:analysis_source_count] == 1 + @test event_results[:stats_geo][:num_truth_sources] == 1 +end + +@testset "reference speed is source-depth scoped" begin + cfg80 = PAMConfig(dx=1e-3, dz=1e-3, axial_dim=0.08, transverse_dim=0.03) + cfg200 = PAMConfig(dx=1e-3, dz=1e-3, axial_dim=0.20, transverse_dim=0.03) + c80 = fill(cfg80.c0, pam_Nx(cfg80), pam_Ny(cfg80)) + c200 = fill(cfg200.c0, pam_Nx(cfg200), pam_Ny(cfg200)) + c80[30:35, :] .= 2500.0 + c200[30:35, :] .= 2500.0 + source = PointSource2D(depth=0.055, lateral=0.0) + + ref80 = TranscranialFUS._pam_reference_sound_speed(c80, cfg80, [source]) + ref200 = TranscranialFUS._pam_reference_sound_speed(c200, cfg200, [source]) + @test ref80 ≈ ref200 + @test mean(c200) < mean(c80) + + rf = zeros(Float64, pam_Ny(cfg80), pam_Nt(cfg80)) + _, _, info = reconstruct_pam( + rf, + c80, + cfg80; + frequencies=[0.5e6], + corrected=false, + reference_sound_speed=1540.0, + axial_step=0.25e-3, + ) + @test info[:reference_sound_speed] == 1540.0 + @test info[:axial_step] ≈ 0.25e-3 + @test info[:axial_substeps_per_cell] == 4 +end diff --git a/test/pam/3d/analysis3d.jl b/test/pam/3d/analysis3d.jl new file mode 100644 index 0000000..57b9850 --- /dev/null +++ b/test/pam/3d/analysis3d.jl @@ -0,0 +1,70 @@ +@testset "detection metrics" begin + cfg = PAMConfig3D( + dx=1e-3, + dy=1e-3, + dz=1e-3, + axial_dim=0.012, + transverse_dim_y=0.012, + transverse_dim_z=0.012, + success_tolerance=0.75e-3, + ) + grid = pam_grid_3d(cfg) + source = PointSource3D(depth=0.004, lateral_y=0.0, lateral_z=0.0, frequency=1e6) + truth_mask = pam_truth_mask_3d([source], grid, cfg; radius=cfg.success_tolerance) + intensity = zeros(Float64, pam_Nx(cfg), pam_Ny(cfg), pam_Nz(cfg)) + truth_idx = source_grid_index_3d(source, cfg) + intensity[truth_idx...] = 1.0 + intensity[end - 1, 2, 2] = 0.8 + + stats = threshold_detection_stats_3d( + intensity, + grid, + cfg, + [source]; + threshold_ratios=[0.5, 0.9], + truth_radius=cfg.success_tolerance, + truth_mask=truth_mask, + ) + + @test length(stats) == 2 + @test stats[1][:predicted_voxels] == 2 + @test stats[1][:precision] ≈ 0.5 + @test stats[1][:source_recall] ≈ 1.0 + @test stats[1][:source_f1] ≈ 2 * 0.5 / 1.5 + @test stats[2][:predicted_voxels] == 1 + @test stats[2][:precision] ≈ 1.0 + @test best_threshold_entry_3d(stats)[:threshold_ratio] == 0.9 + + outlines = threshold_outline_entries_3d(stats) + @test first(outlines).kind == :best_f1 + @test first(outlines).entry[:threshold_ratio] == 0.9 +end + +@testset "localization metrics" begin + cfg = PAMConfig3D( + dx=1e-3, + dy=1e-3, + dz=1e-3, + axial_dim=0.02, + transverse_dim_y=0.012, + transverse_dim_z=0.012, + success_tolerance=1e-3, + peak_suppression_radius=1.5e-3, + PML_GUARD=2, + ) + grid = pam_grid_3d(cfg) + sources = [ + PointSource3D(depth=0.005, lateral_y=0.0, lateral_z=0.0), + PointSource3D(depth=0.010, lateral_y=0.002, lateral_z=-0.002), + ] + intensity = zeros(Float64, pam_Nx(cfg), pam_Ny(cfg), pam_Nz(cfg)) + for (idx, src) in enumerate(sources) + intensity[source_grid_index_3d(src, cfg)...] = 2.0 - 0.25 * idx + end + + stats = analyse_pam_3d(intensity, grid, cfg, sources) + @test stats[:num_success] == length(sources) + @test stats[:success_rate] == 1.0 + @test stats[:mean_radial_error_mm] ≈ 0.0 atol=1e-9 + @test length(stats[:peak_indices]) == length(sources) +end diff --git a/test/pam/3d/config3d.jl b/test/pam/3d/config3d.jl new file mode 100644 index 0000000..3538456 --- /dev/null +++ b/test/pam/3d/config3d.jl @@ -0,0 +1,41 @@ +@testset "config3d" begin + cfg = PAMConfig3D( + dx=1e-3, + dy=0.5e-3, + dz=0.5e-3, + axial_dim=0.04, + transverse_dim_y=0.02, + transverse_dim_z=0.03, + receiver_aperture_y=0.01, + receiver_aperture_z=0.015, + ) + + @test pam_Nx(cfg) == 40 + @test pam_Ny(cfg) == 40 + @test pam_Nz(cfg) == 60 + @test pam_Nt(cfg) == 2000 + @test receiver_row(cfg) == 1 + @test length(receiver_col_range_y(cfg)) == 20 + @test length(receiver_col_range_z(cfg)) == 30 + + grid = pam_grid_3d(cfg) + @test length(grid.x) == pam_Nx(cfg) + @test length(grid.y) == pam_Ny(cfg) + @test length(grid.z) == pam_Nz(cfg) + + src = PointSource3D(depth=0.025, lateral_y=0.001, lateral_z=-0.002) + row, iy, iz = source_grid_index_3d(src, cfg) + @test row == receiver_row(cfg) + 25 + @test grid.y[iy] ≈ src.lateral_y atol=cfg.dy + @test grid.z[iz] ≈ src.lateral_z atol=cfg.dz + + fitted = fit_pam_config_3d( + cfg, + [PointSource3D(depth=0.055)]; + min_bottom_margin=5e-3, + reference_depth=0.03, + ) + @test pam_Nx(fitted) >= receiver_row(cfg) + 60 + @test fitted.axial_dim >= 60e-3 + @test fitted.t_max >= cfg.t_max +end diff --git a/test/pam/3d/medium3d.jl b/test/pam/3d/medium3d.jl new file mode 100644 index 0000000..a71b347 --- /dev/null +++ b/test/pam/3d/medium3d.jl @@ -0,0 +1,34 @@ +@testset "medium3d" begin + cfg = PAMConfig3D( + dx=1e-3, + dy=1e-3, + dz=1e-3, + axial_dim=0.04, + transverse_dim_y=0.03, + transverse_dim_z=0.005, + ) + + c_water, rho_water, info_water = make_pam_medium_3d(cfg; aberrator=:water) + @test size(c_water) == (pam_Nx(cfg), pam_Ny(cfg), pam_Nz(cfg)) + @test size(rho_water) == size(c_water) + @test all(c_water .≈ cfg.c0) + @test all(rho_water .≈ cfg.rho0) + @test info_water[:aberrator] == :water + + hu_vol = synthetic_hu_volume() + c, rho, info = make_pam_medium_3d( + cfg; + aberrator=:skull, + hu_vol=hu_vol, + spacing_m=(1e-3, 1e-3, 1e-3), + slice_index_z=2, + skull_to_transducer=20e-3, + hu_bone_thr=200, + ) + @test size(c) == (pam_Nx(cfg), pam_Ny(cfg), pam_Nz(cfg)) + @test size(rho) == size(c) + @test info[:aberrator] == :skull + @test info[:outer_row] == receiver_row(cfg) + 20 + @test info[:outer_row] < info[:inner_row] + @test maximum(c[info[:outer_row]:info[:inner_row], :, :]) > cfg.c0 +end diff --git a/test/pam/3d/plots3d.jl b/test/pam/3d/plots3d.jl new file mode 100644 index 0000000..5bf7cd9 --- /dev/null +++ b/test/pam/3d/plots3d.jl @@ -0,0 +1,112 @@ +using CairoMakie + +@testset "plot projection helpers" begin + cfg = PAMConfig3D( + dx=1e-3, + dy=1e-3, + dz=1e-3, + axial_dim=0.004, + transverse_dim_y=0.003, + transverse_dim_z=0.005, + ) + grid = pam_grid_3d(cfg) + values = reshape(Float64.(1:(pam_Nx(cfg) * pam_Ny(cfg) * pam_Nz(cfg))), pam_Nx(cfg), pam_Ny(cfg), pam_Nz(cfg)) + + depth_y = TranscranialFUS._project3d_values(values, :depth_y) + depth_z = TranscranialFUS._project3d_values(values, :depth_z) + y_z = TranscranialFUS._project3d_values(values, :y_z) + @test size(depth_y) == (pam_Nx(cfg), pam_Ny(cfg)) + @test size(depth_z) == (pam_Nx(cfg), pam_Nz(cfg)) + @test size(y_z) == (pam_Ny(cfg), pam_Nz(cfg)) + @test y_z == dropdims(maximum(values; dims=1); dims=1) + + xvals, yvals, xlabel, ylabel = TranscranialFUS._projection_axes_3d(grid, cfg, :depth_z) + @test length(xvals) == pam_Nz(cfg) + @test length(yvals) == pam_Nx(cfg) + @test occursin("Z", xlabel) + @test occursin("Depth", ylabel) + @test TranscranialFUS._projection_heatmap_matrix_3d(depth_z, :depth_z) == depth_z' + @test TranscranialFUS._projection_heatmap_matrix_3d(y_z, :y_z) == y_z + + mask = falses(size(values)) + mask[2, 2, 3] = true + @test size(TranscranialFUS._project3d_mask(mask, :depth_y)) == (pam_Nx(cfg), pam_Ny(cfg)) + @test TranscranialFUS._project3d_mask(mask, :depth_y)[2, 2] + @test TranscranialFUS._project3d_mask(mask, :depth_z)[2, 3] + @test TranscranialFUS._project3d_mask(mask, :y_z)[2, 3] + + c = fill(1500.0, size(values)) + c[2, 2, 3] = 2200.0 + @test TranscranialFUS._c_slice_for_projection(c, :depth_y)[2, 2] == 2200.0 + @test TranscranialFUS._c_slice_for_projection(c, :depth_z)[2, 3] == 2200.0 + @test TranscranialFUS._c_slice_for_projection(c, :y_z)[2, 3] == 2200.0 + + sources = [PointSource3D(depth=0.01, lateral_y=0.002, lateral_z=-0.003)] + @test TranscranialFUS.source_triples_mm(sources) == [(10.0, 2.0, -3.0)] + @test_throws ErrorException TranscranialFUS._project3d_values(values, :unknown) + @test_throws ErrorException TranscranialFUS._project3d_mask(mask, :unknown) + @test_throws ErrorException TranscranialFUS._projection_axes_3d(grid, cfg, :unknown) +end + +@testset "3D plot rendering helpers" begin + cfg = PAMConfig3D( + dx=1e-3, + dy=1e-3, + dz=1e-3, + axial_dim=0.008, + transverse_dim_y=0.006, + transverse_dim_z=0.006, + success_tolerance=1e-3, + PML_GUARD=1, + ) + grid = pam_grid_3d(cfg) + source = PointSource3D(depth=0.003, lateral_y=0.0, lateral_z=0.0) + intensity = zeros(Float64, pam_Nx(cfg), pam_Ny(cfg), pam_Nz(cfg)) + intensity[source_grid_index_3d(source, cfg)...] = 1.0 + c = fill(1500.0, size(intensity)) + rho = fill(1000.0, size(intensity)) + + points = TranscranialFUS._voxel_points_3d(intensity .> 0, grid, cfg) + @test length(points.indices) == 1 + @test only(points.depth) ≈ 3.0 + @test only(points.y) ≈ 0.0 + @test only(points.z) ≈ 0.0 + + fig = Figure(size=(300, 240)) + ax = Axis(fig[1, 1]) + @test TranscranialFUS.overlay_skull_3d_projection!(ax, c, collect(grid.y) .* 1e3, depth_coordinates_3d(cfg) .* 1e3, :depth_y) === nothing + @test TranscranialFUS.scatter_sources_3d_projection!(ax, [source], :depth_y) === nothing + + mktempdir() do dir + boundary_path = joinpath(dir, "boundary3d.png") + boundary_metrics = TranscranialFUS.save_threshold_boundary_detection_3d( + boundary_path, + intensity, + 0.75 .* intensity, + grid, + cfg, + [source]; + threshold_ratios=[0.5, 0.9], + truth_radius=cfg.success_tolerance, + c=c, + ) + @test isfile(boundary_path) + @test boundary_metrics["selection_metric"] == "source_f1" + @test boundary_metrics["best_geometric_threshold"] in [0.5, 0.9] + + volume_path = joinpath(dir, "volume3d.png") + volume_metrics = TranscranialFUS.save_best_threshold_volume_3d( + volume_path, + intensity, + grid, + cfg, + [source]; + threshold=0.5, + truth_radius=cfg.success_tolerance, + ) + @test isfile(volume_path) + @test volume_metrics["threshold_ratio"] == 0.5 + @test volume_metrics["predicted_voxels"] == 1 + @test volume_metrics["truth_voxels"] >= 1 + end +end diff --git a/test/pam/3d/reconstruction3d.jl b/test/pam/3d/reconstruction3d.jl new file mode 100644 index 0000000..5dc6ea2 --- /dev/null +++ b/test/pam/3d/reconstruction3d.jl @@ -0,0 +1,124 @@ +@testset "frequency bin selection" begin + dt = 1e-6 + nt = 64 + target_bin = 9 + target_freq = (target_bin - 1) / (nt * dt) + rf = zeros(Float64, 3, 4, nt) + for iy in axes(rf, 1), iz in axes(rf, 2), it in axes(rf, 3) + rf[iy, iz, it] = sin(2 * pi * target_freq * (it - 1) * dt) + end + + freqs, bins = TranscranialFUS._select_frequency_bins_3d(rf, dt, nothing) + @test only(bins) == target_bin + @test only(freqs) ≈ target_freq + + wide_freqs, wide_bins = TranscranialFUS._select_frequency_bins_3d(rf, dt, [target_freq]; bandwidth=2 / (nt * dt)) + @test target_bin in wide_bins + @test length(wide_freqs) >= 2 +end + +@testset "axial gain" begin + cfg = PAMConfig3D(dx=0.5e-3, axial_gain_power=1.5) + intensity = ones(Float64, 4, 2, 2) + + TranscranialFUS._apply_axial_gain_3d!(intensity, cfg) + + @test intensity[1, 1, 1] ≈ 1.0 + @test intensity[2, 1, 1] ≈ 1.0 + @test intensity[3, 1, 1] ≈ 2.0^1.5 + @test intensity[4, 1, 1] ≈ 3.0^1.5 +end + +@testset "reference speed and spectral tapers" begin + cfg = PAMConfig3D(dx=1e-3, axial_dim=0.006, transverse_dim_y=0.004, transverse_dim_z=0.004) + source = PointSource3D(depth=2e-3) + c = fill(1500.0, pam_Nx(cfg), pam_Ny(cfg), pam_Nz(cfg)) + c[3:end, :, :] .= 1800.0 + + @test TranscranialFUS._pam_reference_sound_speed(c, cfg, EmissionSource3D[]; margin=0.0) ≈ mean(c) + @test TranscranialFUS._pam_reference_sound_speed(c, cfg, [source]; margin=0.0) ≈ mean(c[1:3, :, :]) + + shifted = TranscranialFUS._ifftshift_2d([1 2 3; 4 5 6]) + @test shifted == circshift([1 2 3; 4 5 6], (-1, -1)) + + k_radii = [0.0 1.0 1.5 2.0 3.0] + taper = TranscranialFUS._tukey_radial(k_radii, 2.0, 0.5) + @test taper[1] == 1.0 + @test taper[2] == 1.0 + @test 0.0 < taper[3] < 1.0 + @test taper[4] ≈ 0.0 atol=eps(Float64) + @test taper[5] == 0.0 + @test all(TranscranialFUS._tukey_radial(k_radii, 0.0, 0.5) .== 1.0) +end + +@testset "padding helpers" begin + rf = reshape(1:12, 2, 2, 3) + padded, range_y, range_z = TranscranialFUS._zero_pad_receiver_rf_3d(rf, 4, 6) + @test size(padded) == (4, 6, 3) + @test range_y == 2:3 + @test range_z == 3:4 + @test padded[range_y, range_z, :] == rf + @test count(!iszero, padded) == length(rf) + + c = reshape(Float64.(1:8), 2, 2, 2) + edge, cy, cz = TranscranialFUS._edge_pad_lateral_3d(c, 4, 4) + @test size(edge) == (2, 4, 4) + @test cy == 2:3 + @test cz == 2:3 + @test edge[:, cy, cz] == c + @test edge[:, 1:1, cz] == c[:, 1:1, :] + @test edge[:, end:end, cz] == c[:, end:end, :] + + @test_throws ErrorException TranscranialFUS._zero_pad_receiver_rf_3d(rf, 1, 2) + @test_throws ErrorException TranscranialFUS._zero_pad_receiver_rf_3d(rf, 2, 1) + @test_throws ErrorException TranscranialFUS._edge_pad_lateral_3d(c, 1, 2) + @test_throws ErrorException TranscranialFUS._edge_pad_lateral_3d(c, 2, 1) +end + +@testset "small CPU reconstruction metadata" begin + cfg = PAMConfig3D( + dx=1e-3, + dy=1e-3, + dz=1e-3, + axial_dim=0.006, + transverse_dim_y=0.004, + transverse_dim_z=0.004, + dt=1e-6, + t_max=64e-6, + zero_pad_factor=1, + PML_GUARD=1, + axial_gain_power=0.0, + ) + c = fill(Float64(cfg.c0), pam_Nx(cfg), pam_Ny(cfg), pam_Nz(cfg)) + rf = zeros(Float64, pam_Ny(cfg), pam_Nz(cfg), pam_Nt(cfg)) + frequency = 4 / (pam_Nt(cfg) * cfg.dt) + rf[2, 2, :] .= sin.(2π .* frequency .* collect(0:(pam_Nt(cfg) - 1)) .* cfg.dt) + + intensity, grid, info = reconstruct_pam_3d( + rf, + c, + cfg; + frequencies=[frequency], + corrected=false, + reference_sound_speed=cfg.c0, + axial_step=0.5e-3, + time_origin=2e-6, + use_gpu=false, + ) + + @test size(intensity) == size(c) + @test length(grid.t) == pam_Nt(cfg) + @test info[:corrected] == false + @test info[:backend] == :cpu + @test info[:use_gpu] == false + @test info[:reference_sound_speed] == cfg.c0 + @test info[:axial_step] ≈ 0.5e-3 + @test info[:axial_substeps_per_cell] == 2 + @test info[:time_origin] ≈ 2e-6 + @test info[:crop_range_y] == 1:pam_Ny(cfg) + @test info[:crop_range_z] == 1:pam_Nz(cfg) + @test only(info[:frequencies]) ≈ frequency + + @test_throws ErrorException reconstruct_pam_3d(rf[1:(end - 1), :, :], c, cfg; frequencies=[frequency]) + @test_throws ErrorException reconstruct_pam_3d(rf, c, cfg; frequencies=[frequency], reference_sound_speed=0.0) +end diff --git a/test/pam/3d/sources3d.jl b/test/pam/3d/sources3d.jl new file mode 100644 index 0000000..910879a --- /dev/null +++ b/test/pam/3d/sources3d.jl @@ -0,0 +1,50 @@ +@testset "sources3d" begin + point = PointSource3D(depth=0.02, lateral_y=0.001, lateral_z=-0.001, frequency=0.6e6, num_cycles=4) + cluster = BubbleCluster3D(depth=0.03, fundamental=0.5e6, harmonics=[2, 4], harmonic_amplitudes=[1.0, 0.25]) + + @test emission_frequencies(point) == [0.6e6] + @test emission_frequencies(cluster) == [1.0e6, 2.0e6] + @test TranscranialFUS._source_duration(point) ≈ 4 / 0.6e6 + @test TranscranialFUS._source_duration(cluster) ≈ cluster.gate_duration + + signal = TranscranialFUS._source_signal(512, 20e-9, point) + @test any(!iszero, signal) + @test maximum(abs, signal) > 0 + + original = [cluster, point] + resampled = TranscranialFUS._resample_source_phases_3d(original, Random.MersenneTwister(11)) + @test resampled[1].harmonic_phases != cluster.harmonic_phases + @test resampled[2].phase != point.phase +end + +@testset "squiggle and network sources3d" begin + squiggle, squiggle_meta = make_squiggle_bubble_sources_3d( + [(0.03, 0.0, 0.0)]; + root_length=6e-3, + source_spacing=1e-3, + position_jitter=0.0, + min_separation=0.0, + max_sources_per_anchor=12, + rng=Random.MersenneTwister(12), + ) + @test squiggle_meta[:source_model] == :squiggle + @test all(src -> src isa BubbleCluster3D, squiggle) + @test length(squiggle) <= 12 + @test length(squiggle_meta[:centerlines]) == 1 + + network, network_meta = make_network_bubble_sources_3d( + [(0.035, 0.0, 0.0)]; + root_count=3, + generations=1, + branch_length=2e-3, + branch_step=1e-3, + source_spacing=1e-3, + min_separation=0.0, + max_sources_per_center=20, + rng=Random.MersenneTwister(13), + ) + @test network_meta[:source_model] == :network + @test all(src -> src isa BubbleCluster3D, network) + @test !isempty(network_meta[:centerlines]) + @test length(network) <= 20 +end diff --git a/test/pam/3d/workflow3d.jl b/test/pam/3d/workflow3d.jl new file mode 100644 index 0000000..33a4198 --- /dev/null +++ b/test/pam/3d/workflow3d.jl @@ -0,0 +1,45 @@ +@testset "workflow3d rejects CPU reconstruction" begin + cfg = PAMConfig3D( + dx=1e-3, + dy=1e-3, + dz=1e-3, + axial_dim=0.012, + transverse_dim_y=0.008, + transverse_dim_z=0.008, + t_max=20e-6, + ) + c, rho, _ = make_pam_medium_3d(cfg; aberrator=:none) + source = PointSource3D(depth=0.004, frequency=0.5e6) + + @test_throws ErrorException run_pam_case_3d( + c, + rho, + [source], + cfg; + recon_use_gpu=false, + simulation_backend=:analytic, + ) +end + +@testset "analytic RF defaults" begin + cfg = PAMConfig3D( + dx=1e-3, + dy=1e-3, + dz=1e-3, + axial_dim=0.012, + transverse_dim_y=0.006, + transverse_dim_z=0.006, + t_max=20e-6, + dt=0.1e-6, + ) + source = PointSource3D(depth=0.004, frequency=0.5e6, amplitude=1.0, num_cycles=3) + rf, grid, info = TranscranialFUS.analytic_rf_for_point_sources_3d(cfg, [source]) + + @test size(rf) == (pam_Ny(cfg), pam_Nz(cfg), pam_Nt(cfg)) + @test grid === pam_grid_3d(cfg) || length(grid.t) == pam_Nt(cfg) + @test info[:receiver_row] == receiver_row(cfg) + @test info[:receiver_cols_y] == receiver_col_range_y(cfg) + @test info[:receiver_cols_z] == receiver_col_range_z(cfg) + @test info[:source_indices] == [source_grid_index_3d(source, cfg)] + @test any(!iszero, rf) +end diff --git a/test/pam/helpers.jl b/test/pam/helpers.jl new file mode 100644 index 0000000..9343a47 --- /dev/null +++ b/test/pam/helpers.jl @@ -0,0 +1,42 @@ +function pam_gpu_maps_approx(a, b; rtol::Real=1e-2) + scale = max(maximum(abs.(a)), maximum(abs.(b)), 1.0) + return isapprox(a, b; rtol=rtol, atol=1e-5 * scale) +end + +function synthetic_hu_volume(nslices::Int=5, rows::Int=80, cols::Int=60) + hu = fill(Float32(-1000), nslices, rows, cols) + for z in 1:nslices + hu[z, 24:30, 20:40] .= 1200 + end + return hu +end + +function analytic_rf_for_point_source(cfg::PAMConfig, src::PointSource2D) + kgrid = pam_grid(cfg) + nt = pam_Nt(cfg) + rf = zeros(Float64, kgrid.Ny, nt) + duration = src.num_cycles / src.frequency + base_t = collect(0:(nt - 1)) .* cfg.dt + + for j in 1:kgrid.Ny + distance = hypot(src.depth, kgrid.y_vec[j] - src.lateral) + arrival = src.delay + distance / cfg.c0 + local_t = base_t .- arrival + active = findall((local_t .>= 0.0) .& (local_t .<= duration)) + isempty(active) && continue + env = TranscranialFUS._tukey_window(length(active), 0.25) + rf[j, active] .= (src.amplitude / sqrt(max(distance, cfg.dx))) .* env .* sin.(2π .* src.frequency .* local_t[active] .+ src.phase) + end + return rf, kgrid +end + +function capture_stderr_result(f::Function) + mktemp() do _, io + result = redirect_stderr(io) do + f() + end + flush(io) + seekstart(io) + return result, read(io, String) + end +end diff --git a/test/pam/integration.jl b/test/pam/integration.jl new file mode 100644 index 0000000..38e42e1 --- /dev/null +++ b/test/pam/integration.jl @@ -0,0 +1,30 @@ +@testset "optional k-Wave smoke" begin + if get(ENV, "TRANSCRANIALFUS_RUN_KWAVE_TESTS", "0") == "1" && kwave_available() + pam_cfg = PAMConfig( + dx=0.5e-3, + dz=0.5e-3, + axial_dim=0.03, + transverse_dim=0.03, + receiver_aperture=0.03, + PML_GUARD=20, + t_max=30e-6, + dt=50e-9, + zero_pad_factor=2, + peak_suppression_radius=1.0e-3, + success_tolerance=1.5e-3, + ) + c_pam, rho_pam, _ = make_pam_medium(pam_cfg; aberrator=:none) + sources = [PointSource2D(depth=0.015, lateral=0.003, frequency=0.4e6, amplitude=5e4, num_cycles=4)] + rf, kgrid_pam, sim_info = simulate_point_sources(c_pam, rho_pam, sources, pam_cfg) + @test size(rf) == (pam_Ny(pam_cfg), pam_Nt(pam_cfg)) + @test sim_info[:receiver_row] == receiver_row(pam_cfg) + @test sim_info[:receiver_row] == 1 + + pam_map, _, pam_info = reconstruct_pam(rf, c_pam, pam_cfg; frequencies=[0.4e6], corrected=false) + pam_stats = analyse_pam_2d(pam_map, kgrid_pam, pam_cfg, sources) + @test pam_info[:corrected] == false + @test pam_stats[:mean_radial_error_mm] <= 1.5 + else + @info "Skipping PAM k-Wave smoke test. Set TRANSCRANIALFUS_RUN_KWAVE_TESTS=1 to enable it." + end +end diff --git a/test/pam/runtests.jl b/test/pam/runtests.jl new file mode 100644 index 0000000..046d7fd --- /dev/null +++ b/test/pam/runtests.jl @@ -0,0 +1,38 @@ +using Test +using Random +using Statistics +using FFTW +using TranscranialFUS + +isdefined(Main, :synthetic_hu_volume) || include("helpers.jl") + +@testset "PAM" begin + @testset "2D" begin + include("2d/config.jl") + include("2d/sources.jl") + include("2d/medium.jl") + include("2d/reconstruction.jl") + include("2d/analysis.jl") + include("2d/workflow.jl") + include("2d/plots.jl") + end + + @testset "3D" begin + include("3d/config3d.jl") + include("3d/sources3d.jl") + include("3d/medium3d.jl") + include("3d/reconstruction3d.jl") + include("3d/analysis3d.jl") + include("3d/workflow3d.jl") + include("3d/plots3d.jl") + end + + @testset "Setup" begin + include("setup/config.jl") + include("setup/sources.jl") + include("setup/medium.jl") + include("setup/summary.jl") + end + + include("integration.jl") +end diff --git a/test/pam/setup/config.jl b/test/pam/setup/config.jl new file mode 100644 index 0000000..5fee909 --- /dev/null +++ b/test/pam/setup/config.jl @@ -0,0 +1,57 @@ +@testset "CLI config parsing" begin + opts, provided = TranscranialFUS.parse_cli(String[]) + @test isempty(provided) + @test opts["dimension"] == "2" + @test opts["source-model"] == "squiggle" + @test opts["recon-progress"] == "false" + @test TranscranialFUS.parse_bool(opts["recon-progress"]) == false + + opts3, provided3 = TranscranialFUS.parse_cli(["--dimension=3"]) + @test "dimension" in provided3 + @test opts3["source-model"] == "point" + @test opts3["sources-mm"] == "30:0:0" + @test opts3["frequency-mhz"] == "0.5" + + @test TranscranialFUS.parse_dimension("2d") == 2 + @test TranscranialFUS.parse_dimension("3D") == 3 + @test_throws ErrorException TranscranialFUS.parse_dimension("4") + @test TranscranialFUS.parse_float_list("1, 2.5,,3") == [1.0, 2.5, 3.0] + @test TranscranialFUS.parse_int_list("1, 3,5") == [1, 3, 5] + @test TranscranialFUS.parse_threshold_ratios("0.5,0.2,0.5") == [0.2, 0.5] + @test_throws ErrorException TranscranialFUS.parse_threshold_ratios("") + + search_opts = copy(opts) + search_opts["auto-threshold-min"] = "0.2" + search_opts["auto-threshold-max"] = "0.5" + search_opts["auto-threshold-step"] = "0.2" + @test TranscranialFUS.parse_threshold_search_ratios(search_opts) == [0.2, 0.4, 0.5] + + @test TranscranialFUS.parse_source_model("point") == :point + @test TranscranialFUS.parse_aberrator("water") == :water + @test TranscranialFUS.parse_simulation_backend("analytic") == :analytic + @test TranscranialFUS.parse_source_phase_mode("random-phase-per-window") == :random_phase_per_window + @test TranscranialFUS.parse_analysis_mode("auto", :squiggle) == :detection + @test TranscranialFUS.parse_analysis_mode("auto", :point) == :localization + @test TranscranialFUS.parse_window_taper("rectangular") == :rectangular + @test TranscranialFUS.parse_receiver_aperture_mm("full") === nothing + @test TranscranialFUS.parse_receiver_aperture_mm("25") ≈ 25e-3 + @test TranscranialFUS.parse_transducer_mm("-30:5") == (-30e-3, 5e-3) +end + +@testset "window and output config" begin + opts, _ = TranscranialFUS.parse_cli(["--recon-window-us=12", "--recon-hop-us=6", "--recon-window-taper=tukey"]) + win = TranscranialFUS.make_window_config(opts, :windowed) + @test win.enabled + @test win.window_duration ≈ 12e-6 + @test win.hop ≈ 6e-6 + @test win.taper == :tukey + + sources = [PointSource2D(depth=0.03, lateral=0.0, frequency=0.4e6)] + cfg = PAMConfig(axial_dim=0.04, transverse_dim=0.03) + out = TranscranialFUS.default_output_dir(opts, sources, cfg, Dict("source_model" => :point)) + @test occursin("run_pam_2d", out) + @test occursin("point", out) + + @test occursin("reconstruct_example_run", basename(TranscranialFUS.default_reconstruction_output_dir("/tmp/example_run"))) + @test_throws ErrorException TranscranialFUS.reject_cached_simulation_options!(Set(["sources-mm"]), ["sources-mm", "dx-mm"]) +end diff --git a/test/pam/setup/medium.jl b/test/pam/setup/medium.jl new file mode 100644 index 0000000..30f069c --- /dev/null +++ b/test/pam/setup/medium.jl @@ -0,0 +1,50 @@ +@testset "simulation defaults" begin + cfg2 = PAMConfig(dx=1e-3, dz=1e-3, axial_dim=0.02, transverse_dim=0.01, receiver_aperture=0.004) + info2 = TranscranialFUS.default_simulation_info(cfg2) + @test info2[:receiver_row] == receiver_row(cfg2) + @test info2[:receiver_cols] == receiver_col_range(cfg2) + @test isempty(info2[:source_indices]) + + cfg3 = PAMConfig3D( + dx=1e-3, + dy=1e-3, + dz=1e-3, + axial_dim=0.02, + transverse_dim_y=0.01, + transverse_dim_z=0.012, + receiver_aperture_y=0.004, + receiver_aperture_z=0.006, + ) + info3 = TranscranialFUS.default_simulation_info(cfg3) + @test info3[:receiver_row] == receiver_row(cfg3) + @test info3[:receiver_cols_y] == receiver_col_range_y(cfg3) + @test info3[:receiver_cols_z] == receiver_col_range_z(cfg3) + @test isempty(info3[:source_indices]) +end + +@testset "default frequencies and analytic sampling" begin + sources = [ + PointSource2D(depth=0.01, lateral=0.0, frequency=0.4e6), + BubbleCluster2D(depth=0.02, lateral=0.0, fundamental=0.5e6, harmonics=[2, 3]), + ] + @test TranscranialFUS.default_recon_frequencies(sources) == [0.4e6, 1.0e6, 1.5e6] + @test TranscranialFUS._sample_source_signal([0.0, 10.0], 0.5, 1.0) ≈ 5.0 + @test TranscranialFUS._sample_source_signal([1.0], -1.0, 1.0) == 0.0 + + cfg3 = PAMConfig3D( + dx=1e-3, + dy=1e-3, + dz=1e-3, + axial_dim=0.012, + transverse_dim_y=0.006, + transverse_dim_z=0.006, + t_max=20e-6, + dt=0.1e-6, + ) + src3 = PointSource3D(depth=0.004, frequency=0.5e6, amplitude=1.0, num_cycles=3) + rf, grid, info = TranscranialFUS.analytic_rf_for_point_sources_3d(cfg3, [src3]) + @test size(rf) == (pam_Ny(cfg3), pam_Nz(cfg3), pam_Nt(cfg3)) + @test length(grid.t) == pam_Nt(cfg3) + @test info[:source_indices] == [source_grid_index_3d(src3, cfg3)] + @test any(!iszero, rf) +end diff --git a/test/pam/setup/sources.jl b/test/pam/setup/sources.jl new file mode 100644 index 0000000..b8cedb0 --- /dev/null +++ b/test/pam/setup/sources.jl @@ -0,0 +1,74 @@ +@testset "source parsing" begin + opts, _ = TranscranialFUS.parse_cli([ + "--source-model=point", + "--sources-mm=30:-2,40:3", + "--source-frequencies-mhz=0.4,0.6", + "--source-amplitudes-pa=1.5", + "--phases-deg=0,90", + "--delays-us=0,2", + ]) + sources, meta = TranscranialFUS.parse_point_sources(opts) + @test length(sources) == 2 + @test sources[1].depth ≈ 30e-3 + @test sources[2].lateral ≈ 3e-3 + @test sources[2].frequency ≈ 0.6e6 + @test all(src.amplitude ≈ 1.5 for src in sources) + @test sources[2].phase ≈ pi / 2 + @test sources[2].delay ≈ 2e-6 + @test meta["source_model"] == "point" + @test meta["physical_source_count"] == 2 + + opts3, _ = TranscranialFUS.parse_cli([ + "--dimension=3", + "--source-model=point", + "--sources-mm=30:1:-1,35:2:3", + "--source-frequencies-mhz=0.5", + ]) + sources3, meta3 = TranscranialFUS.parse_point_sources_3d(opts3) + @test length(sources3) == 2 + @test sources3[1].lateral_y ≈ 1e-3 + @test sources3[1].lateral_z ≈ -1e-3 + @test all(src.frequency ≈ 0.5e6 for src in sources3) + @test meta3["source_model"] == "point3d" + + @test TranscranialFUS.expand_source_values(Float64[], 3, 2.0) == [2.0, 2.0, 2.0] + @test TranscranialFUS.expand_source_values([4.0], 2, 0.0) == [4.0, 4.0] + @test_throws ErrorException TranscranialFUS.expand_source_values([1.0, 2.0], 3, 0.0) + @test TranscranialFUS.parse_coordinate_pairs_mm("10:1,20:-2", "sources-mm") == [(10e-3, 1e-3), (20e-3, -2e-3)] + @test TranscranialFUS.parse_coordinate_triples_mm("10:1:2", "sources-mm") == [(10e-3, 1e-3, 2e-3)] +end + +@testset "squiggle source parsing" begin + opts, _ = TranscranialFUS.parse_cli([ + "--source-model=squiggle", + "--anchors-mm=35:0", + "--vascular-length-mm=4", + "--vascular-source-spacing-mm=1", + "--vascular-position-jitter-mm=0", + "--vascular-min-separation-mm=0", + "--vascular-max-sources-per-anchor=8", + ]) + cfg = PAMConfig(transverse_dim=0.02) + sources, meta = TranscranialFUS.parse_squiggle_sources(opts, cfg) + @test !isempty(sources) + @test length(sources) <= 8 + @test meta["source_model"] == "squiggle" + @test haskey(meta, "squiggle") + + cfg3 = PAMConfig3D(transverse_dim_y=0.02, transverse_dim_z=0.02) + opts3, _ = TranscranialFUS.parse_cli([ + "--dimension=3", + "--source-model=squiggle", + "--anchors-mm=35:0:0", + "--vascular-length-mm=4", + "--vascular-source-spacing-mm=1", + "--vascular-position-jitter-mm=0", + "--vascular-min-separation-mm=0", + "--vascular-max-sources-per-anchor=8", + ]) + sources3, meta3 = TranscranialFUS.parse_squiggle_sources_3d(opts3, cfg3) + @test !isempty(sources3) + @test length(sources3) <= 8 + @test meta3["source_model"] == "squiggle3d" + @test haskey(meta3, "squiggle") +end diff --git a/test/pam/setup/summary.jl b/test/pam/setup/summary.jl new file mode 100644 index 0000000..55df479 --- /dev/null +++ b/test/pam/setup/summary.jl @@ -0,0 +1,47 @@ +@testset "summary helpers" begin + p2 = PointSource2D(depth=0.01, lateral=0.002, frequency=0.4e6, amplitude=2.0, phase=0.3, delay=1e-6, num_cycles=4) + b2 = BubbleCluster2D(depth=0.02, lateral=-0.001, fundamental=0.5e6, harmonics=[2], harmonic_amplitudes=[0.7]) + p3 = PointSource3D(depth=0.03, lateral_y=0.001, lateral_z=-0.002, frequency=0.6e6) + b3 = BubbleCluster3D(depth=0.04, lateral_y=0.002, lateral_z=0.003, fundamental=0.5e6) + + @test source_summary(p2)["kind"] == "point" + @test source_summary(b2)["kind"] == "bubble_cluster" + @test source_summary(p3)["kind"] == "point3d" + @test source_summary(b3)["kind"] == "bubble3d" + + @test TranscranialFUS.source_model_from_meta(Dict("source_model" => "vascular"), [b2]) == :squiggle + @test TranscranialFUS.source_model_from_meta(Dict("source_model" => "network3d"), [b3]) == :network + @test TranscranialFUS.source_model_from_meta(Dict{String, Any}(), [p2]) == :point + @test TranscranialFUS.source_model_from_meta(Dict{String, Any}(), [b2]) == :squiggle + + meta = Dict( + "squiggle" => Dict( + "centerlines_m" => [ + [[0.01, -0.001], [0.02, 0.001]], + ], + ), + ) + centerlines = TranscranialFUS.centerlines_from_emission_meta(meta) + @test length(centerlines) == 1 + @test first(centerlines)[1] == (0.01, -0.001) + + cfg = PAMConfig(dx=1e-3, dz=1e-3, axial_dim=0.04, transverse_dim=0.02) + mask = TranscranialFUS.detection_truth_mask_from_meta(meta, pam_grid(cfg), cfg, 1e-3) + @test count(mask) > 0 + + info = Dict( + :total_window_count => 2, + :used_window_count => 1, + :skipped_window_count => 1, + :window_samples => 100, + :hop_samples => 50, + :energy_threshold => 0.1, + :used_window_ranges => [1:100], + :skipped_window_ranges => [51:150], + :accumulation => :intensity, + ) + compact = TranscranialFUS.compact_window_info(info) + @test compact["used_window_ranges"] == [[1, 100]] + @test compact["skipped_window_ranges"] == [[51, 150]] + @test compact["accumulation"] == "intensity" +end diff --git a/test/runtests.jl b/test/runtests.jl index 204c9ab..adafa6b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,79 +4,7 @@ using Statistics using FFTW using TranscranialFUS -function pam_gpu_maps_approx(a, b; rtol::Real=1e-2) - scale = max(maximum(abs.(a)), maximum(abs.(b)), 1.0) - return isapprox(a, b; rtol=rtol, atol=1e-5 * scale) -end - -function synthetic_hu_volume(nslices::Int=5, rows::Int=80, cols::Int=60) - hu = fill(Float32(-1000), nslices, rows, cols) - for z in 1:nslices - hu[z, 24:30, 20:40] .= 1200 - end - return hu -end - -function analytic_rf_for_point_source(cfg::PAMConfig, src::PointSource2D) - kgrid = pam_grid(cfg) - nt = pam_Nt(cfg) - rf = zeros(Float64, kgrid.Ny, nt) - duration = src.num_cycles / src.frequency - base_t = collect(0:(nt - 1)) .* cfg.dt - - for j in 1:kgrid.Ny - distance = hypot(src.depth, kgrid.y_vec[j] - src.lateral) - arrival = src.delay + distance / cfg.c0 - local_t = base_t .- arrival - active = findall((local_t .>= 0.0) .& (local_t .<= duration)) - isempty(active) && continue - env = TranscranialFUS._tukey_window(length(active), 0.25) - rf[j, active] .= (src.amplitude / sqrt(max(distance, cfg.dx))) .* env .* sin.(2π .* src.frequency .* local_t[active] .+ src.phase) - end - return rf, kgrid -end - -function fake_pam_sweep_runner(c, rho, sources, cfg; frequencies=nothing, use_gpu=false) - src = only(sources) - kgrid = pam_grid(cfg) - depth_mm = src.depth * 1e3 - lateral_mm = src.lateral * 1e3 - geo_error_mm = depth_mm / 100 + abs(lateral_mm) / 10 - hasa_error_mm = geo_error_mm / 2 - - stats_geo = Dict{Symbol, Any}( - :predicted_mm => [(depth_mm, lateral_mm + geo_error_mm)], - :mean_radial_error_mm => geo_error_mm, - :mean_norm_peak_intensity => 0.6, - ) - stats_hasa = Dict{Symbol, Any}( - :predicted_mm => [(depth_mm, lateral_mm + hasa_error_mm)], - :mean_radial_error_mm => hasa_error_mm, - :mean_norm_peak_intensity => 0.8, - ) - - return Dict{Symbol, Any}( - :rf => zeros(Float64, pam_Ny(cfg), pam_Nt(cfg)), - :kgrid => kgrid, - :simulation => Dict{Symbol, Any}(:receiver_row => receiver_row(cfg)), - :pam_geo => fill(geo_error_mm, pam_Nx(cfg), pam_Ny(cfg)), - :pam_hasa => fill(hasa_error_mm, pam_Nx(cfg), pam_Ny(cfg)), - :stats_geo => stats_geo, - :stats_hasa => stats_hasa, - :reconstruction_frequencies => isnothing(frequencies) ? [src.frequency] : collect(Float64.(frequencies)), - ) -end - -function capture_stderr_result(f::Function) - mktemp() do _, io - result = redirect_stderr(io) do - f() - end - flush(io) - seekstart(io) - return result, read(io, String) - end -end +include("pam/helpers.jl") @testset "HU conversion" begin hu = Float32[-1000 0 300 1200] @@ -184,6 +112,9 @@ end @test TranscranialFUS._unwrap_phase(wrapped) ≈ truth atol=1e-10 end +include("common/kwave_wrapper.jl") +include("scripts/run_pam.jl") + @testset "Focus analysis" begin cfg = SimulationConfig(z_focus=0.02, x_focus=0.0, dx=0.5e-3, dz=0.5e-3, transverse_dim=0.03, trans_aperture=0.01, axial_padding=2.5) cfg.trans_index = Nx(cfg) - cfg.PML_GUARD @@ -203,814 +134,7 @@ end @test stats[:focal_area_mm2] > 0 end -@testset "PAM reconstruction in water" begin - cfg = PAMConfig( - dx=0.5e-3, - dz=0.5e-3, - axial_dim=0.04, - transverse_dim=0.03, - receiver_aperture=nothing, - t_max=40e-6, - dt=50e-9, - PML_GUARD=5, - zero_pad_factor=2, - peak_suppression_radius=1e-3, - success_tolerance=1.0e-3, - ) - source = PointSource2D(depth=0.015, lateral=0.003, frequency=0.4e6, amplitude=1.0, num_cycles=5) - c, _, _ = make_pam_medium(cfg; aberrator=:none) - rf, kgrid = analytic_rf_for_point_source(cfg, source) - cuda_ok = TranscranialFUS._pam_cuda_functional() - - quiet_result, quiet_progress = capture_stderr_result() do - reconstruct_pam(rf, c, cfg; frequencies=[source.frequency], corrected=false) - end - intensity, _, info = quiet_result - @test quiet_progress == "" - stats = analyse_pam_2d(intensity, kgrid, cfg, [source]) - hasa_result, hasa_progress = capture_stderr_result() do - reconstruct_pam( - rf, - c, - cfg; - frequencies=[source.frequency], - corrected=true, - use_gpu=cuda_ok, - show_progress=true, - ) - end - intensity_hasa, _, info_hasa = hasa_result - stats_hasa = analyse_pam_2d(intensity_hasa, kgrid, cfg, [source]) - one_window = PAMWindowConfig( - enabled=true, - window_duration=pam_Nt(cfg) * cfg.dt, - hop=pam_Nt(cfg) * cfg.dt, - taper=:none, - min_energy_ratio=0.0, - ) - windowed_result, windowed_progress = capture_stderr_result() do - reconstruct_pam_windowed( - rf, - c, - cfg; - frequencies=[source.frequency], - corrected=false, - window_config=one_window, - use_gpu=cuda_ok, - show_progress=true, - ) - end - intensity_windowed, _, info_windowed = windowed_result - cropped_range = 101:500 - cropped_origin = (first(cropped_range) - 1) * cfg.dt - intensity_cropped, _, info_cropped = reconstruct_pam( - rf[:, cropped_range], - c, - cfg; - frequencies=[source.frequency], - use_gpu=cuda_ok, - corrected=false, - time_origin=cropped_origin, - ) - stats_cropped = analyse_pam_2d(intensity_cropped, kgrid, cfg, [source]) - - @test info[:corrected] == false - @test info[:show_progress] == false - @test stats[:mean_radial_error_mm] < 1.0 - @test stats[:success_rate] == 1.0 - @test stats[:mean_norm_peak_intensity] > 0.5 - @test info_hasa[:corrected] == true - @test info_hasa[:use_gpu] == cuda_ok - @test info_hasa[:backend] == (cuda_ok ? :cuda : :cpu) - @test info_hasa[:gpu_precision] == (cuda_ok ? Float32 : nothing) - @test info_hasa[:show_progress] == true - @test occursin("PAM HASA reconstruction", hasa_progress) - # GPU path batches all frequencies together and emits a single march summary; - # CPU path still reports per-frequency progress. - if cuda_ok - @test occursin("freq batch march elapsed", hasa_progress) - else - @test occursin("frequency 1/", hasa_progress) - end - @test occursin("total elapsed", hasa_progress) - @test stats_hasa[:mean_radial_error_mm] < 1.0 - @test stats_hasa[:success_rate] == 1.0 - if cuda_ok - @test pam_gpu_maps_approx(intensity_windowed, intensity) - intensity_hasa_cpu, _, _ = reconstruct_pam( - rf, - c, - cfg; - frequencies=[source.frequency], - corrected=true, - use_gpu=false, - ) - @test pam_gpu_maps_approx(intensity_hasa, intensity_hasa_cpu) - - c_lens = copy(c) - c_lens[20:30, 25:35] .= 1700.0 - intensity_lens_cpu, _, _ = reconstruct_pam( - rf, - c_lens, - cfg; - frequencies=[source.frequency], - corrected=true, - use_gpu=false, - ) - intensity_lens_gpu, _, info_lens_gpu = reconstruct_pam( - rf, - c_lens, - cfg; - frequencies=[source.frequency], - corrected=true, - use_gpu=true, - ) - @test info_lens_gpu[:backend] == :cuda - @test pam_gpu_maps_approx(intensity_lens_gpu, intensity_lens_cpu) - else - @test intensity_windowed ≈ intensity - err = try - reconstruct_pam(rf, c, cfg; frequencies=[source.frequency], corrected=false, use_gpu=true) - nothing - catch err - err - end - @test err isa ErrorException - @test occursin("functional NVIDIA CUDA GPU", sprint(showerror, err)) - end - @test info_windowed[:use_gpu] == cuda_ok - @test info_windowed[:backend] == (cuda_ok ? :cuda : :cpu) - @test info_windowed[:show_progress] == true - @test occursin("PAM geometric ASA windowed reconstruction", windowed_progress) - @test occursin("window 1/1", windowed_progress) - @test occursin("complete", windowed_progress) - @test info_windowed[:used_window_count] == 1 - @test info_windowed[:skipped_window_count] == 0 - @test info_cropped[:time_origin] ≈ cropped_origin - @test info_cropped[:use_gpu] == cuda_ok - @test info_cropped[:backend] == (cuda_ok ? :cuda : :cpu) - @test stats_cropped[:success_rate] == 1.0 - - cached_results = reconstruct_pam_case( - rf, - c, - [source], - cfg; - simulation_info=Dict(:receiver_row => receiver_row(cfg), :receiver_cols => receiver_col_range(cfg)), - frequencies=[source.frequency], - use_gpu=cuda_ok, - ) - @test cached_results[:simulation][:receiver_row] == receiver_row(cfg) - @test cached_results[:use_gpu] == cuda_ok - @test cached_results[:show_progress] == false - @test cached_results[:geo_info][:use_gpu] == cuda_ok - @test cached_results[:hasa_info][:use_gpu] == cuda_ok - @test cached_results[:geo_info][:backend] == (cuda_ok ? :cuda : :cpu) - @test cached_results[:hasa_info][:backend] == (cuda_ok ? :cuda : :cpu) - @test cached_results[:stats_geo][:success_rate] == 1.0 - @test cached_results[:stats_hasa][:success_rate] == 1.0 - - windowed_results = reconstruct_pam_case( - rf, - c, - [source], - cfg; - simulation_info=Dict(:receiver_row => receiver_row(cfg), :receiver_cols => receiver_col_range(cfg)), - frequencies=[source.frequency], - reconstruction_mode=:windowed, - window_config=one_window, - ) - @test windowed_results[:reconstruction_mode] == :windowed - @test windowed_results[:geo_info][:used_window_count] == 1 - - duplicate_source_events = [source, PointSource2D(depth=source.depth, lateral=source.lateral, frequency=source.frequency)] - truth_mask = pam_truth_mask([source], kgrid, cfg; radius=cfg.success_tolerance) - event_results = reconstruct_pam_case( - rf, - c, - duplicate_source_events, - cfg; - simulation_info=Dict(:receiver_row => receiver_row(cfg), :receiver_cols => receiver_col_range(cfg)), - frequencies=[source.frequency], - analysis_mode=:detection, - detection_truth_mask=truth_mask, - analysis_sources=[source], - ) - @test event_results[:analysis_source_count] == 1 - @test event_results[:stats_geo][:num_truth_sources] == 1 - - pam_script = read(joinpath(@__DIR__, "..", "scripts", "run_pam.jl"), String) - @test occursin("\"recon-progress\" => \"false\"", pam_script) - @test occursin("show_progress=parse_bool(opts[\"recon-progress\"])", pam_script) -end - -@testset "PAM windowing helpers" begin - cfg = PAMWindowConfig(enabled=true, window_duration=10e-6, hop=5e-6) - exact_ranges, exact_win, exact_hop = TranscranialFUS._pam_window_ranges(100, 0.1e-6, cfg) - @test exact_ranges == [1:100] - @test exact_win == 100 - @test exact_hop == 50 - - overlap_ranges, overlap_win, overlap_hop = TranscranialFUS._pam_window_ranges(250, 0.1e-6, cfg) - @test overlap_win == 100 - @test overlap_hop == 50 - @test overlap_ranges == [1:100, 51:150, 101:200, 151:250] - - short_ranges, short_win, short_hop = TranscranialFUS._pam_window_ranges(40, 0.1e-6, cfg) - @test short_ranges == [1:40] - @test short_win == 40 - @test short_hop == 50 - - p1 = fill(1.0 + 0.0im, 2, 2) - p2 = fill(-1.0 + 0.0im, 2, 2) - @test all(abs2.(p1 .+ p2) .== 0.0) - @test all(abs2.(p1) .+ abs2.(p2) .== 2.0) - @test TranscranialFUS.pam_reconstruction_mode(:auto, :squiggle) == :windowed - @test TranscranialFUS.pam_reconstruction_mode(:auto, :point) == :full - @test TranscranialFUS.pam_reconstruction_mode(:full, :squiggle) == :full -end - -@testset "PAM 3D frequency bin selection" begin - dt = 1e-6 - nt = 64 - target_bin = 9 - target_freq = (target_bin - 1) / (nt * dt) - rf = zeros(Float64, 3, 4, nt) - for iy in axes(rf, 1), iz in axes(rf, 2), it in axes(rf, 3) - rf[iy, iz, it] = sin(2 * pi * target_freq * (it - 1) * dt) - end - - freqs, bins = TranscranialFUS._select_frequency_bins_3d(rf, dt, nothing) - @test only(bins) == target_bin - @test only(freqs) ≈ target_freq -end - -@testset "PAM 3D axial gain" begin - cfg = PAMConfig3D(dx=0.5e-3, axial_gain_power=1.5) - intensity = ones(Float64, 4, 2, 2) - - TranscranialFUS._apply_axial_gain_3d!(intensity, cfg) - - @test intensity[1, 1, 1] ≈ 1.0 - @test intensity[2, 1, 1] ≈ 1.0 - @test intensity[3, 1, 1] ≈ 2.0^1.5 - @test intensity[4, 1, 1] ≈ 3.0^1.5 -end - -@testset "PAM analysis metrics" begin - cfg = PAMConfig( - dx=0.5e-3, - dz=0.5e-3, - axial_dim=0.05, - transverse_dim=0.04, - receiver_aperture=nothing, - PML_GUARD=5, - peak_suppression_radius=2e-3, - success_tolerance=1.5e-3, - ) - kgrid = pam_grid(cfg) - depth = depth_coordinates(kgrid, cfg) - lateral = kgrid.y_vec - sources = [ - PointSource2D(depth=0.015, lateral=-0.004, frequency=0.4e6), - PointSource2D(depth=0.028, lateral=0.006, frequency=0.4e6), - ] - - intensity = zeros(Float64, kgrid.Nx, kgrid.Ny) - σd = 0.8e-3 - σl = 0.8e-3 - for src in sources - for i in 1:kgrid.Nx, j in 1:kgrid.Ny - intensity[i, j] += exp(-((depth[i] - src.depth)^2 / (2σd^2) + (lateral[j] - src.lateral)^2 / (2σl^2))) - end - end - - stats = analyse_pam_2d(intensity, kgrid, cfg, sources) - @test stats[:mean_radial_error_mm] < 0.6 - @test stats[:num_success] == 2 - @test length(stats[:axial_fwhm_mm]) == 2 - @test all(stats[:axial_fwhm_mm] .> 0) - @test all(stats[:lateral_fwhm_mm] .> 0) - - metrics = pam_intensity_metrics(intensity, kgrid, cfg; threshold_ratio=0.5, reference_intensity=2.0) - @test metrics[:peak_intensity] ≈ maximum(intensity) - @test metrics[:relative_peak_intensity] ≈ maximum(intensity) / 2.0 - @test metrics[:integrated_intensity_m2] > 0 - @test metrics[:active_area_mm2] > 0 - @test isfinite(metrics[:centroid_depth_mm]) - @test isfinite(metrics[:centroid_lateral_mm]) -end - -@testset "Gaussian pulse cluster emissions" begin - dt = 20e-9 - nt = 1000 - src = GaussianPulseCluster2D( - depth=0.03, - lateral=0.0, - fundamental=0.5e6, - amplitude=1.0, - n_bubbles=1.0, - harmonics=[2], - harmonic_amplitudes=[1.0], - harmonic_phases=[0.0], - gate_duration=10e-6, - ) - signal = TranscranialFUS._source_signal(nt, dt, src) - active = findall(!iszero, signal) - - @test !isempty(active) - @test maximum(abs, signal) > 0.5 - @test abs(signal[first(active)]) < 0.05 * maximum(abs, signal) - @test abs(signal[last(active)]) < 0.05 * maximum(abs, signal) - @test emission_frequencies(src) == [1.0e6] - @test cavitation_model(src) == :gaussian_pulse - - spectrum = abs.(fft(signal)) - freq_axis = collect(0:(nt - 1)) ./ (nt * dt) - pos_bins = 2:(fld(nt, 2) + 1) - peak_bin = pos_bins[argmax(spectrum[pos_bins])] - @test abs(freq_axis[peak_bin] - 1.0e6) <= 1 / (nt * dt) - - harmonic = BubbleCluster2D( - depth=0.03, - lateral=0.0, - fundamental=0.5e6, - harmonics=[2], - harmonic_amplitudes=[1.0], - harmonic_phases=[0.0], - gate_duration=10e-6, - ) - @test cavitation_model(harmonic) == :harmonic_cos - @test TranscranialFUS._normalize_cavitation_model("harmonic-cos") == :harmonic_cos - @test TranscranialFUS._normalize_cavitation_model("gaussian-pulse") == :gaussian_pulse - @test_throws ErrorException TranscranialFUS._normalize_cavitation_model("haromnic-cos") -end - -@testset "Source phase modes" begin - @test TranscranialFUS._normalize_source_phase_mode(:coherent) == :coherent - @test TranscranialFUS._normalize_source_phase_mode(:random_static_phase) == :random_static_phase - @test TranscranialFUS._normalize_source_phase_mode(:random_phase_per_window) == :random_phase_per_window - @test TranscranialFUS._normalize_source_phase_mode(:random_phase_per_realization) == :random_phase_per_realization - @test TranscranialFUS._normalize_source_phase_mode("random-phase-per-realization") == :random_phase_per_realization - @test_throws ErrorException TranscranialFUS._normalize_source_phase_mode(:unknown_mode) - - @test TranscranialFUS._normalize_cluster_phase_mode(:random_static_phase) == :random - @test TranscranialFUS._normalize_cluster_phase_mode("random_static_phase") == :random - @test TranscranialFUS._normalize_cluster_phase_mode(:coherent) == :coherent - - rng_r = Random.MersenneTwister(7) - sources_orig = [ - BubbleCluster2D(depth=0.03, lateral=0.0, fundamental=0.5e6, - harmonics=[2, 3], harmonic_amplitudes=[1.0, 0.6], - harmonic_phases=[0.1, 0.2], gate_duration=10e-6), - PointSource2D(depth=0.02, lateral=0.005, frequency=1.0e6, phase=0.5), - GaussianPulseCluster2D(depth=0.04, lateral=-0.005, fundamental=0.5e6, - harmonics=[2], harmonic_amplitudes=[1.0], harmonic_phases=[0.3], - gate_duration=10e-6), - ] - resampled = TranscranialFUS._resample_source_phases(sources_orig, rng_r) - - @test resampled[1].depth == sources_orig[1].depth - @test resampled[1].lateral == sources_orig[1].lateral - @test resampled[1].harmonic_phases != sources_orig[1].harmonic_phases - @test resampled[2].phase != sources_orig[2].phase - @test resampled[3].harmonic_phases != sources_orig[3].harmonic_phases - -end - -@testset "SourceVariabilityConfig" begin - rng = Random.MersenneTwister(42) - src = BubbleCluster2D(depth=0.03, lateral=0.0, fundamental=0.5e6, - harmonics=[2, 3], harmonic_amplitudes=[1.0, 0.6], - harmonic_phases=[0.1, 0.2], gate_duration=50e-6) - - expanded, n = TranscranialFUS._expand_sources_per_window( - [src], 10e-6, 5e-6, 80e-6, Random.MersenneTwister(1)) - @test n == 15 - @test length(expanded) == 15 - @test all(s.amplitude == src.amplitude for s in expanded) - @test all(s.fundamental == src.fundamental for s in expanded) - - # frequency jitter: fundamentals vary across copies - exp_fj, _ = TranscranialFUS._expand_sources_per_window( - [src], 10e-6, 5e-6, 80e-6, Random.MersenneTwister(99); - variability=SourceVariabilityConfig(frequency_jitter_fraction=0.05)) - @test length(unique(round.(Float64[s.fundamental for s in exp_fj]; digits=0))) > 1 -end - -@testset "Squiggle bubble sources" begin - squiggle_clusters, squiggle_meta = make_squiggle_bubble_sources( - [(0.03, 0.0)]; - root_length=12e-3, - squiggle_amplitude=1.5e-3, - squiggle_wavelength=6e-3, - source_spacing=1e-3, - position_jitter=0.0, - min_separation=0.0, - lateral_bounds=(-0.02, 0.02), - rng=Random.MersenneTwister(41), - ) - - @test squiggle_meta[:source_model] == :squiggle - @test squiggle_meta[:cavitation_model] == :harmonic_cos - @test all(src -> src isa BubbleCluster2D, squiggle_clusters) - @test length(squiggle_meta[:centerlines]) == 1 - @test maximum(src.lateral for src in squiggle_clusters) - minimum(src.lateral for src in squiggle_clusters) > 10e-3 - @test maximum(src.depth for src in squiggle_clusters) - minimum(src.depth for src in squiggle_clusters) > 2e-3 - - pulse_clusters, pulse_meta = make_squiggle_bubble_sources( - [(0.03, 0.0)]; - cavitation_model=:gaussian_pulse, - root_length=12e-3, - squiggle_amplitude=1.5e-3, - squiggle_wavelength=6e-3, - source_spacing=1e-3, - position_jitter=0.0, - min_separation=0.0, - lateral_bounds=(-0.02, 0.02), - rng=Random.MersenneTwister(41), - ) - - @test pulse_meta[:cavitation_model] == :gaussian_pulse - @test all(src -> src isa GaussianPulseCluster2D, pulse_clusters) - @test length(pulse_clusters) == length(squiggle_clusters) - - multi_clusters, multi_meta = make_squiggle_bubble_sources( - [(0.03, -0.004), (0.035, 0.004)]; - root_length=8e-3, - source_spacing=1e-3, - position_jitter=0.0, - min_separation=0.0, - max_sources_per_anchor=20, - lateral_bounds=(-0.02, 0.02), - rng=Random.MersenneTwister(43), - ) - @test length(multi_clusters) > length(squiggle_clusters) - @test multi_meta[:source_model] == :squiggle - @test length(multi_meta[:centerlines]) == 2 -end - -@testset "PAM detection metrics" begin - cfg = PAMConfig( - dx=0.5e-3, - dz=0.5e-3, - axial_dim=0.05, - transverse_dim=0.04, - receiver_aperture=nothing, - PML_GUARD=5, - success_tolerance=1.0e-3, - ) - kgrid = pam_grid(cfg) - sources = [ - PointSource2D(depth=0.015, lateral=-0.004, frequency=0.4e6), - PointSource2D(depth=0.028, lateral=0.006, frequency=0.4e6), - ] - intensity = zeros(Float64, kgrid.Nx, kgrid.Ny) - row1, col1 = source_grid_index(sources[1], cfg, kgrid) - row_false, col_false = source_grid_index(PointSource2D(depth=0.038, lateral=-0.012), cfg, kgrid) - intensity[row1, col1] = 1.0 - intensity[row_false, col_false] = 0.8 - - stats = analyse_pam_detection_2d( - intensity, - kgrid, - cfg, - sources; - truth_radius=1.0e-3, - threshold_ratio=0.5, - frequencies=[0.4e6], - psf_axial_fwhm=2.0e-3, - psf_lateral_fwhm=2.0e-3, - ) - - @test 0 < stats[:precision] < 1 - @test 0 < stats[:recall] < 1 - @test stats[:false_positive_pixels] > 0 - @test stats[:false_negative_pixels] > 0 - @test stats[:spurious_prediction_components] == 1 - @test 0 < stats[:energy_fraction_inside_mask] < 1 - @test stats[:energy_fraction_inside_mask] + stats[:energy_fraction_outside_mask] ≈ 1.0 - @test stats[:energy_fraction_inside_predicted_mask] > stats[:energy_fraction_inside_mask] - @test stats[:energy_fraction_inside_predicted_mask] + stats[:energy_fraction_outside_predicted_mask] ≈ 1.0 - @test isfinite(stats[:centroid_error_mm]) - @test stats[:axial_spread_mm] > 0 - @test stats[:lateral_spread_mm] > 0 - @test haskey(stats, :psf_target_correlation) - @test isfinite(stats[:psf_target_normalized_l2_error]) - - truth_override = falses(kgrid.Nx, kgrid.Ny) - truth_override[row_false, col_false] = true - override_stats = analyse_pam_detection_2d( - intensity, - kgrid, - cfg, - sources; - truth_radius=1.0e-3, - threshold_ratio=0.5, - truth_mask=truth_override, - psf_axial_fwhm=2.0e-3, - psf_lateral_fwhm=2.0e-3, - ) - @test override_stats[:truth_mask_mode] == :provided - @test override_stats[:psf_target_mode] == :provided_mask - @test override_stats[:true_positive_pixels] == 1 - @test override_stats[:false_positive_pixels] == 1 - @test override_stats[:false_negative_pixels] == 0 - - source_map = pam_source_map(sources, kgrid, cfg; weights=:uniform) - @test sum(source_map) == length(sources) - blurred_truth = pam_psf_blurred_truth_map( - sources, - kgrid, - cfg; - psf_axial_fwhm=2.0e-3, - psf_lateral_fwhm=2.0e-3, - weights=:uniform, - ) - @test size(blurred_truth) == size(intensity) - @test sum(blurred_truth) ≈ sum(source_map) atol=1e-8 -end - -@testset "CLEAN peak detection" begin - # Two sources whose focal shoulders overlap: argmax with a suppression - # radius smaller than the focus picks sidelobes of one source instead of - # the second source. CLEAN should find both. - cfg = PAMConfig( - dx=0.5e-3, - dz=0.5e-3, - axial_dim=0.05, - transverse_dim=0.04, - receiver_aperture=40e-3, - PML_GUARD=5, - peak_suppression_radius=1e-3, # smaller than focus FWHM - success_tolerance=2e-3, - ) - kgrid = pam_grid(cfg) - depth = depth_coordinates(kgrid, cfg) - lateral = kgrid.y_vec - truths = [ - PointSource2D(depth=0.02, lateral=-0.003, frequency=0.4e6), - PointSource2D(depth=0.02, lateral=0.003, frequency=0.4e6), - ] - intensity = zeros(Float64, kgrid.Nx, kgrid.Ny) - σd = 3e-3 # broad axial focus to break argmax - σl = 2e-3 - # unequal amplitudes to mimic a real coherent-interference scene - amps = (1.0, 0.7) - for (src, amp) in zip(truths, amps) - for i in 1:kgrid.Nx, j in 1:kgrid.Ny - intensity[i, j] += amp * exp(-((depth[i] - src.depth)^2 / (2σd^2) + (lateral[j] - src.lateral)^2 / (2σl^2))) - end - end - - stats_argmax = analyse_pam_2d(intensity, kgrid, cfg, truths; peak_method=:argmax) - stats_clean = analyse_pam_2d( - intensity, kgrid, cfg, truths; - peak_method=:clean, - frequencies=[0.4e6], - clean_psf_axial_fwhm=2.355 * σd, - clean_psf_lateral_fwhm=2.355 * σl, - ) - - @test stats_clean[:mean_radial_error_mm] < stats_argmax[:mean_radial_error_mm] - @test stats_clean[:num_success] == 2 -end - -@testset "PAM sweep target presets" begin - preset, axial, lateral = TranscranialFUS._resolve_pam_sweep_targets(:paper) - @test preset == :paper - @test axial == [30.0, 40.0, 50.0, 60.0, 70.0, 80.0] - @test lateral == [-20.0, -10.0, 0.0, 10.0, 20.0] - - preset, axial, lateral = TranscranialFUS._resolve_pam_sweep_targets(:quick) - @test preset == :quick - @test axial == [40.0, 60.0, 80.0] - @test lateral == [-10.0, 0.0, 10.0] - - preset, axial, lateral = TranscranialFUS._resolve_pam_sweep_targets( - :paper; - axial_targets_mm=[55.0, 35.0], - lateral_targets_mm=[10.0, -5.0, 0.0], - ) - @test preset == :custom - @test axial == [35.0, 55.0] - @test lateral == [-5.0, 0.0, 10.0] - - @test_throws ErrorException TranscranialFUS._resolve_pam_sweep_targets(:custom) - @test_throws ErrorException TranscranialFUS._resolve_pam_sweep_targets(:paper; axial_targets_mm=[40.0]) -end - -@testset "PAM sweep example selection" begin - targets = vec([ - PointSource2D(depth=axial_mm * 1e-3, lateral=lateral_mm * 1e-3, frequency=1e6) - for axial_mm in (40.0, 60.0, 80.0), lateral_mm in (-10.0, 0.0, 10.0) - ]) - examples = TranscranialFUS._default_pam_sweep_examples(targets) - @test examples == [(40.0, 0.0), (60.0, 0.0), (80.0, 0.0)] -end - -@testset "PAM sweep aggregation" begin - cfg = PAMConfig( - dx=1e-3, - dz=1e-3, - axial_dim=0.09, - transverse_dim=0.05, - receiver_aperture=0.03, - t_max=20e-6, - dt=100e-9, - ) - targets = vec([ - PointSource2D(depth=axial_mm * 1e-3, lateral=lateral_mm * 1e-3, frequency=1e6) - for axial_mm in (40.0, 60.0), lateral_mm in (-10.0, 0.0, 10.0) - ]) - c, rho, _ = make_pam_medium(cfg; aberrator=:none) - sweep = run_pam_sweep( - c, - rho, - targets, - cfg; - frequencies=[1e6], - example_targets_mm=[(40.0, 0.0), (60.0, 0.0)], - runner=fake_pam_sweep_runner, - ) - - @test sweep[:axial_targets_mm] == [40.0, 60.0] - @test sweep[:lateral_targets_mm] == [-10.0, 0.0, 10.0] - @test size(sweep[:geo_error_mm]) == (2, 3) - @test size(sweep[:hasa_error_mm]) == (2, 3) - @test sweep[:geo_error_mm][1, 1] ≈ 1.4 - @test sweep[:geo_error_mm][2, 3] ≈ 1.6 - @test sweep[:hasa_error_mm][1, 2] ≈ 0.2 - @test length(sweep[:cases]) == 6 - @test length(sweep[:example_cases]) == 2 - @test sweep[:example_targets_mm] == [(40.0, 0.0), (60.0, 0.0)] -end - -@testset "PAM external PML sizing" begin - cfg_fine = PAMConfig(dx=0.2e-3, dz=0.2e-3, axial_dim=0.03, transverse_dim=0.03) - cfg_coarse = PAMConfig(dx=0.5e-3, dz=0.5e-3, axial_dim=0.03, transverse_dim=0.03) - - @test TranscranialFUS._pam_pml_guard(cfg_fine) == 20 - @test TranscranialFUS._pam_pml_guard(cfg_coarse) == 8 - @test receiver_row(cfg_coarse) == 1 - - kgrid = pam_grid(cfg_coarse) - src = PointSource2D(depth=0.015, lateral=0.003, frequency=0.4e6) - row, _ = source_grid_index(src, cfg_coarse, kgrid) - @test row == 31 - @test row <= pam_Nx(cfg_coarse) -end - -@testset "PAM skull sweep setup" begin - base_cfg = PAMConfig(dx=1e-3, dz=1e-3, axial_dim=0.04, transverse_dim=0.06) - targets = [ - PointSource2D(depth=axial_mm * 1e-3, lateral=0.0, frequency=1e6) - for axial_mm in (40.0, 60.0, 80.0) - ] - fitted_cfg = fit_pam_config( - base_cfg, - targets; - min_bottom_margin=5e-3, - reference_depth=30e-3, - ) - hu_vol = synthetic_hu_volume() - c, _, info = make_pam_medium( - fitted_cfg; - aberrator=:skull, - hu_vol=hu_vol, - spacing_m=(1e-3, 1e-3, 1e-3), - slice_index=2, - skull_to_transducer=30e-3, - hu_bone_thr=200, - ) - - @test info[:outer_row] == receiver_row(fitted_cfg) + 30 - @test info[:outer_row] < info[:inner_row] - for src in targets - row, col = source_grid_index(src, fitted_cfg, pam_grid(fitted_cfg)) - @test row > info[:inner_row] - @test row <= pam_Nx(fitted_cfg) - @test c[row, col] ≈ fitted_cfg.c0 - end -end - -@testset "PAM skull target filtering" begin - cfg = PAMConfig(dx=1e-3, dz=1e-3, axial_dim=0.09, transverse_dim=0.06) - hu_vol = synthetic_hu_volume() - c, _, info = make_pam_medium( - cfg; - aberrator=:skull, - hu_vol=hu_vol, - spacing_m=(1e-3, 1e-3, 1e-3), - slice_index=2, - skull_to_transducer=30e-3, - hu_bone_thr=200, - ) - - targets = PointSource2D[ - PointSource2D(depth=(info[:inner_row] - 2) * 1e-3, lateral=0.0, frequency=1e6), - PointSource2D(depth=(info[:inner_row] + 5) * 1e-3, lateral=0.0, frequency=1e6), - PointSource2D(depth=(info[:inner_row] + 8) * 1e-3, lateral=25e-3, frequency=1e6), - ] - - valid_targets, dropped_targets, cavity_start_rows = TranscranialFUS._filter_pam_targets_in_skull_cavity( - c, - cfg, - targets; - min_margin=1e-3, - ) - - @test length(valid_targets) == 1 - @test only(valid_targets).lateral ≈ 0.0 - @test length(dropped_targets) == 2 - @test Set(drop[:reason] for drop in dropped_targets) == Set((:too_shallow_for_cavity, :no_skull_above)) - center_col = source_grid_index(only(valid_targets), cfg, pam_grid(cfg))[2] - @test cavity_start_rows[center_col] == info[:inner_row] + 2 -end - -@testset "PAM config fitting and skull placement" begin - base_cfg = PAMConfig(dx=1e-3, dz=1e-3, axial_dim=0.03, transverse_dim=0.06) - sources = [PointSource2D(depth=0.04, lateral=0.0)] - fitted_cfg = fit_pam_config( - base_cfg, - sources; - min_bottom_margin=5e-3, - reference_depth=30e-3, - ) - - @test pam_Nx(fitted_cfg) == 46 - @test fitted_cfg.axial_dim ≈ 46e-3 - - hu_vol = synthetic_hu_volume() - c, rho, info = make_pam_medium( - fitted_cfg; - aberrator=:skull, - hu_vol=hu_vol, - spacing_m=(1e-3, 1e-3, 1e-3), - slice_index=2, - skull_to_transducer=30e-3, - hu_bone_thr=200, - ) - - @test size(c) == (pam_Nx(fitted_cfg), pam_Ny(fitted_cfg)) - @test size(rho) == size(c) - @test info[:outer_row] == receiver_row(fitted_cfg) + 30 - @test info[:outer_row] < info[:inner_row] - @test maximum(c[info[:outer_row]:info[:inner_row], :]) > fitted_cfg.c0 - - source_row, source_col = source_grid_index(first(sources), fitted_cfg, pam_grid(fitted_cfg)) - @test c[source_row, source_col] ≈ fitted_cfg.c0 - @test all(c[(source_row + 1):end, source_col] .≈ fitted_cfg.c0) -end - -@testset "PAM deep-domain fitting is padding-stable" begin - base_cfg = PAMConfig( - dx=1e-3, - dz=1e-3, - axial_dim=0.08, - transverse_dim=0.06, - receiver_aperture=0.04, - t_max=80e-6, - dt=50e-9, - ) - cluster = BubbleCluster2D( - depth=0.08, - lateral=0.0, - fundamental=0.5e6, - gate_duration=50e-6, - ) - fitted_cfg = fit_pam_config(base_cfg, [cluster]; min_bottom_margin=5e-3) - @test fitted_cfg.t_max >= TranscranialFUS._required_pam_t_max(base_cfg, [cluster]) - @test fitted_cfg.t_max > 110e-6 - - cfg80 = PAMConfig(dx=1e-3, dz=1e-3, axial_dim=0.08, transverse_dim=0.03) - cfg200 = PAMConfig(dx=1e-3, dz=1e-3, axial_dim=0.20, transverse_dim=0.03) - c80 = fill(cfg80.c0, pam_Nx(cfg80), pam_Ny(cfg80)) - c200 = fill(cfg200.c0, pam_Nx(cfg200), pam_Ny(cfg200)) - c80[30:35, :] .= 2500.0 - c200[30:35, :] .= 2500.0 - source = PointSource2D(depth=0.055, lateral=0.0) - - ref80 = TranscranialFUS._pam_reference_sound_speed(c80, cfg80, [source]) - ref200 = TranscranialFUS._pam_reference_sound_speed(c200, cfg200, [source]) - @test ref80 ≈ ref200 - @test mean(c200) < mean(c80) - - rf = zeros(Float64, pam_Ny(cfg80), pam_Nt(cfg80)) - _, _, info = reconstruct_pam( - rf, - c80, - cfg80; - frequencies=[0.5e6], - corrected=false, - reference_sound_speed=1540.0, - axial_step=0.25e-3, - ) - @test info[:reference_sound_speed] == 1540.0 - @test info[:axial_step] ≈ 0.25e-3 - @test info[:axial_substeps_per_cell] == 4 - @test TranscranialFUS._pam_axial_substeps(0.2e-3, 50e-6) == 4 -end +include("pam/runtests.jl") @testset "k-Wave smoke tests" begin if get(ENV, "TRANSCRANIALFUS_RUN_KWAVE_TESTS", "0") == "1" && kwave_available() @@ -1036,51 +160,6 @@ end @test size(p_ts, 1) == Nx(cfg) @test size(p_ts, 2) == Nz(cfg) @test size(p_ts, 3) == Nt(cfg) - - pam_cfg = PAMConfig( - dx=0.5e-3, - dz=0.5e-3, - axial_dim=0.03, - transverse_dim=0.03, - receiver_aperture=0.03, - PML_GUARD=20, - t_max=30e-6, - dt=50e-9, - zero_pad_factor=2, - peak_suppression_radius=1.0e-3, - success_tolerance=1.5e-3, - ) - c_pam, rho_pam, _ = make_pam_medium(pam_cfg; aberrator=:none) - sources = [PointSource2D(depth=0.015, lateral=0.003, frequency=0.4e6, amplitude=5e4, num_cycles=4)] - rf, kgrid_pam, sim_info = simulate_point_sources(c_pam, rho_pam, sources, pam_cfg) - @test size(rf) == (pam_Ny(pam_cfg), pam_Nt(pam_cfg)) - @test sim_info[:receiver_row] == receiver_row(pam_cfg) - @test sim_info[:receiver_row] == 1 - - pam_map, _, pam_info = reconstruct_pam(rf, c_pam, pam_cfg; frequencies=[0.4e6], corrected=false) - pam_stats = analyse_pam_2d(pam_map, kgrid_pam, pam_cfg, sources) - @test pam_info[:corrected] == false - @test pam_stats[:mean_radial_error_mm] <= 1.5 - - sweep_sources = [ - PointSource2D(depth=0.012, lateral=-0.003, frequency=0.4e6, amplitude=5e4, num_cycles=4), - PointSource2D(depth=0.012, lateral=0.003, frequency=0.4e6, amplitude=5e4, num_cycles=4), - PointSource2D(depth=0.018, lateral=-0.003, frequency=0.4e6, amplitude=5e4, num_cycles=4), - PointSource2D(depth=0.018, lateral=0.003, frequency=0.4e6, amplitude=5e4, num_cycles=4), - ] - sweep_results = run_pam_sweep( - c_pam, - rho_pam, - sweep_sources, - pam_cfg; - frequencies=[0.4e6], - example_targets_mm=[(12.0, -3.0), (12.0, 3.0), (18.0, 0.0 + 3.0)], - ) - @test size(sweep_results[:geo_error_mm]) == (2, 2) - @test size(sweep_results[:hasa_error_mm]) == (2, 2) - @test all(isfinite.(vec(sweep_results[:geo_error_mm]))) - @test all(isfinite.(vec(sweep_results[:hasa_error_mm]))) - @test length(sweep_results[:cases]) == 4 else @info "Skipping k-Wave smoke tests. Set TRANSCRANIALFUS_RUN_KWAVE_TESTS=1 to enable them." end diff --git a/test/scripts/run_pam.jl b/test/scripts/run_pam.jl new file mode 100644 index 0000000..4c46e4d --- /dev/null +++ b/test/scripts/run_pam.jl @@ -0,0 +1,56 @@ +@testset "run_pam script entrypoint" begin + include("../../scripts/run_pam.jl") + + mktempdir() do dir + out2d = joinpath(dir, "out2d") + dry2d_args = [ + "--source-model=point", + "--sources-mm=30:0", + "--out-dir=$out2d", + "--boundary-threshold-ratios=0.4,0.8", + ] + dry2d = TranscranialFUS.run_pam_dry_plan(dry2d_args) + @test dry2d[:branch] == :pam2d_simulation + @test dry2d[:out_dir] == out2d + @test dry2d[:source_model] == :point + @test dry2d[:source_count] == 1 + @test dry2d[:threshold_ratios] == [0.4, 0.8] + @test main(dry2d_args; dry_run=true) == dry2d + + medium_summary = TranscranialFUS.run_pam_medium_summary(Dict(:mask => trues(2, 2), :aberrator => :none, :rows => 2)) + @test medium_summary == Dict("aberrator" => :none, "rows" => 2) + + out3d = joinpath(dir, "out3d") + dry3d = TranscranialFUS.run_pam_dry_plan([ + "--dimension=3", + "--source-model=point", + "--sources-mm=20:0:0", + "--out-dir=$out3d", + "--auto-threshold-search=true", + "--auto-threshold-min=0.2", + "--auto-threshold-max=0.5", + "--auto-threshold-step=0.2", + ]) + @test dry3d[:branch] == :pam3d + @test dry3d[:out_dir] == out3d + @test dry3d[:source_model] == :point + @test dry3d[:threshold_score_ratios] == [0.2, 0.4, 0.5] + + cached_dir = joinpath(dir, "cached") + mkpath(cached_dir) + cached_path = joinpath(cached_dir, "result.jld2") + write(cached_path, UInt8[]) + cached = main([ + "--from-run-dir=$cached_dir", + "--out-dir=$(joinpath(dir, "cached_out"))", + ]; dry_run=true) + @test cached[:branch] == :pam2d_cached + @test cached[:cached_path] == cached_path + @test cached[:reconstruction_source]["mode"] == "cached_rf" + + @test_throws ErrorException main(["--dimension=3", "--from-run-dir=$cached_dir"]; dry_run=true) + @test_throws ErrorException main(["--dimension=3", "--aberrator=water"]; dry_run=true) + @test_throws ErrorException main(["--from-run-dir=$(joinpath(dir, "missing"))"]; dry_run=true) + @test_throws ErrorException main(["--from-run-dir=$cached_dir", "--sources-mm=30:0"]; dry_run=true) + end +end diff --git a/validation/2D_PAM_Accuracy/plot_results.jl b/validation/2D_PAM_Accuracy/plot_results.jl new file mode 100644 index 0000000..89a003f --- /dev/null +++ b/validation/2D_PAM_Accuracy/plot_results.jl @@ -0,0 +1,273 @@ +""" +Reproduce Fig. 7 of Schoen & Arvanitis (2020) from saved validation results. + +Usage: + julia --project=. validation/2D_PAM_Accuracy/plot_results.jl [results_file.jld2] +""" + +using Pkg +Pkg.activate(joinpath(@__DIR__, "..", "..")) + +using CairoMakie, JLD2, TranscranialFUS, Statistics, Printf + +# ── Load results ────────────────────────────────────────────────────────────── + +results_dir = @__DIR__ +results_file = if !isempty(ARGS) + ARGS[1] +else + files = filter(f -> startswith(f, "results_") && endswith(f, ".jld2"), readdir(results_dir)) + isempty(files) && error("No results_*.jld2 in $results_dir. Run run_validation.jl first.") + joinpath(results_dir, last(sort(files))) +end +println("Loading: $results_file") + +d = load(results_file) +geo_errors_mm = d["geo_errors_mm"] +hasa_errors_mm = d["hasa_errors_mm"] +AXIAL_MM = d["AXIAL_MM"] +LATERAL_MM = d["LATERAL_MM"] +APERTURES_MM = d["APERTURES_MM"] +DX = d["DX"] +DT = d["DT"] +T_MAX = d["T_MAX"] +AXIAL_DIM = d["AXIAL_DIM"] +TRANS_DIM = d["TRANS_DIM"] +SKULL_DIST = d["SKULL_DIST"] +SLICE_INDEX = d["SLICE_INDEX"] + +# ── Rebuild medium for panel (a) ────────────────────────────────────────────── + +MAX_APT_MM = maximum(APERTURES_MM) +sim_cfg = PAMConfig( + dx = DX, + dz = DX, + axial_dim = AXIAL_DIM, + transverse_dim = TRANS_DIM, + receiver_aperture = MAX_APT_MM * 1e-3, + t_max = T_MAX, + dt = DT, +) + +println("Rebuilding skull medium…") +c, _, _ = make_pam_medium( + sim_cfg; + aberrator = :skull, + skull_to_transducer = SKULL_DIST, + slice_index = SLICE_INDEX, +) + +# ── Coordinate arrays ───────────────────────────────────────────────────────── + +Nx, Ny = size(c) +R = receiver_row(sim_cfg) +dx_mm = DX * 1e3 + +ax_coords = [(i - R) * dx_mm for i in 1:Nx] +lat_coords = [(j - (Ny ÷ 2 + 1)) * dx_mm for j in 1:Ny] + +# ── Aperture colours (100→red, 75→green, 50→teal) ──────────────────────────── + +apt_colors = Dict( + 100.0 => RGBf(0.80, 0.20, 0.15), + 75.0 => RGBf(0.15, 0.60, 0.15), + 50.0 => RGBf(0.10, 0.65, 0.70), +) + +# ── Error colour / size helpers ─────────────────────────────────────────────── + +MAX_ERR = 8.0 +MIN_MS = 2.5 +MAX_MS = 15.0 +errcmap = :plasma + +err_to_ms(e) = MIN_MS + (MAX_MS - MIN_MS) * clamp(e, 0.0, MAX_ERR) / MAX_ERR + +function plot_error_grid!(ax, errors_pi, axial_mm, lateral_mm) + for (ai, az) in enumerate(axial_mm), (li, lat) in enumerate(lateral_mm) + e = errors_pi[ai, li] + isnan(e) && continue + scatter!(ax, [lat], [az]; + color = clamp(e, 0.0, MAX_ERR), + colorrange = (0.0, MAX_ERR), + colormap = errcmap, + markersize = err_to_ms(e), + strokewidth = 0, + ) + end +end + +ax_lim_dep = (maximum(AXIAL_MM) + 5.0, minimum(AXIAL_MM) - 5.0) # yreversed order + +# ── Theme ───────────────────────────────────────────────────────────────────── + +update_theme!( + fontsize = 9, + Axis = ( + xgridvisible = false, + ygridvisible = false, + topspinevisible = false, + rightspinevisible = false, + ), +) + +fig = Figure(size = (920, 420)) + +# ── Panel (a): skull with source grid ──────────────────────────────────────── + +# Visible depth range: just above skull to deepest source +depth_top_mm = SKULL_DIST * 1e3 - 8.0 # a bit above the skull +depth_bot_mm = maximum(AXIAL_MM) + 5.0 + +# Bar y-coords (above skull, negative depth = between receiver and skull) +bar_bot_mm = depth_top_mm - 6.0 +bar_top_mm = depth_top_mm - 2.5 +bar_mid_mm = (bar_bot_mm + bar_top_mm) / 2.0 + +ax_a = Axis(fig[1:2, 1]; + xlabel = "Transverse Position [mm]", + ylabel = "Axial Position [mm]", + aspect = DataAspect(), + yreversed = true, + title = "a", + titlealign = :left, + titlefont = :bold, +) + +# Skull sound-speed heatmap +heatmap!(ax_a, lat_coords, ax_coords, c'; + colormap = :grays, + colorrange = (1400, 2100), +) + +# Source dots +for az in AXIAL_MM, lat in LATERAL_MM + scatter!(ax_a, [lat], [az]; color = :black, markersize = 4) +end + +# Aperture bars: widest drawn first so narrower bars sit on top +for apt_mm in sort(APERTURES_MM; rev=true) + half_mm = apt_mm / 2.0 + band!(ax_a, [-half_mm, half_mm], bar_bot_mm, bar_top_mm; + color = (apt_colors[apt_mm], 0.9)) +end + +# Labels: placed visually above the bars (smaller y = higher with yreversed) +# bar_bot_mm is the visual top of the bar; go further up (smaller y) +label_y = bar_bot_mm - 2.5 +# Place each label in its bar's exclusive outer zone (half-widths: 50, 37.5, 25 mm) +label_xs = [48.0, 31.0, 0.0] # 100 mm → outer wing; 75 mm → mid ring; 50 mm → centre +for (xi, apt_mm) in zip(label_xs, sort(APERTURES_MM; rev=true)) + text!(ax_a, xi, label_y; + text = "$(round(Int, apt_mm)) mm", + align = (:center, :center), + color = apt_colors[apt_mm], + fontsize = 7, + font = :bold, + ) +end + +xlims!(ax_a, -56.0, 56.0) +ylims!(ax_a, depth_bot_mm, label_y - 1.5) # yreversed: first=visual bottom + +# Sound-speed colorbar: small horizontal strip above panel (a) +Colorbar(fig[0, 1]; + colormap = :grays, + colorrange = (1400, 2100), + label = "Sound Speed [m/s]", + vertical = false, + height = 10, + ticklabelsize = 7, + labelsize = 8, + ticks = [1400, 1700, 2100], + tellwidth = false, +) + +# ── Panels (b) and (c) ──────────────────────────────────────────────────────── + +for (pi, apt_mm) in enumerate(APERTURES_MM) + tcol = apt_colors[apt_mm] + gcol = pi + 1 # figure columns 2..4 + + show_xlabel = pi == 2 + show_ylabel_b = pi == 1 + show_ylabel_c = pi == 1 + + # ---- b: uncorrected ---- + ax_b = Axis(fig[1, gcol]; + xlabel = show_xlabel ? "Transverse Position [mm]" : "", + ylabel = show_ylabel_b ? "Axial Position [mm]" : "", + yreversed = true, + aspect = DataAspect(), + title = "$(round(Int, apt_mm)) mm", + titlecolor = tcol, + xticklabelsize = 7, + yticklabelsize = 7, + ) + plot_error_grid!(ax_b, geo_errors_mm[pi, :, :], AXIAL_MM, LATERAL_MM) + xlims!(ax_b, -25.0, 25.0) + ylims!(ax_b, ax_lim_dep...) + + # ---- c: corrected ---- + ax_c = Axis(fig[2, gcol]; + xlabel = show_xlabel ? "Transverse Position [mm]" : "", + ylabel = show_ylabel_c ? "Axial Position [mm]" : "", + yreversed = true, + aspect = DataAspect(), + xticklabelsize = 7, + yticklabelsize = 7, + ) + plot_error_grid!(ax_c, hasa_errors_mm[pi, :, :], AXIAL_MM, LATERAL_MM) + xlims!(ax_c, -25.0, 25.0) + ylims!(ax_c, ax_lim_dep...) +end + +Label(fig[0, 2:4], "Uncorrected"; + fontsize = 11, font = :bold, + color = RGBf(0.45, 0.0, 0.55), + halign = :center, + valign = :bottom, + padding = (0, 0, 0, 4), +) +Label(fig[2, 2:4, Top()], "Corrected"; + fontsize = 11, font = :bold, + color = RGBf(0.70, 0.30, 0.05), + halign = :center, + padding = (0, 0, 6, 0), +) + +# ── Shared error colorbar ───────────────────────────────────────────────────── + +Colorbar(fig[1:2, 5]; + colormap = errcmap, + colorrange = (0.0, MAX_ERR), + label = "Error [mm]", + ticklabelsize = 7, + labelsize = 8, + width = 12, + ticks = 0:2:Int(MAX_ERR), +) + +# ── Spacing ─────────────────────────────────────────────────────────────────── + +colgap!(fig.layout, 5) +rowgap!(fig.layout, 3) + +# ── Print table ─────────────────────────────────────────────────────────────── + +println() +println(" Aperture │ Geo (uncorrected) │ HASA (corrected) │ Paper HASA") +println(" ─────────┼──────────────────────┼───────────────────────┼────────────────") +ref = Dict(50.0=>"1.2 ± 0.7", 75.0=>"0.9 ± 0.5", 100.0=>"0.8 ± 0.4") +for (pi, apt_mm) in enumerate(APERTURES_MM) + gv = filter(!isnan, geo_errors_mm[pi, :, :]) + hv = filter(!isnan, hasa_errors_mm[pi, :, :]) + @printf(" %3.0f mm │ %4.1f ± %4.1f mm │ %4.1f ± %4.1f mm │ %s mm\n", + apt_mm, mean(gv), std(gv), mean(hv), std(hv), ref[apt_mm]) +end + +# ── Save ────────────────────────────────────────────────────────────────────── + +outfile = replace(results_file, r"\.jld2$" => "_fig7.pdf") +save(outfile, fig) +println("\nSaved → $outfile") diff --git a/validation/2D_PAM_Accuracy/results.md b/validation/2D_PAM_Accuracy/results.md new file mode 100644 index 0000000..ba4cb51 --- /dev/null +++ b/validation/2D_PAM_Accuracy/results.md @@ -0,0 +1,16 @@ +# 2D PAM Localization Accuracy — Schoen & Arvanitis (2020) Reproduction + +**Setup:** 54 point sources (6 axial × 9 lateral, 30–80 mm depth, ±20 mm lateral), +simulated through a human skull CT slice (slice 250, skull at 20 mm) with k-Wave. +Frequency: 1 MHz, 10 cycles. Grid: 200 µm pitch, 40 ns time step. +Reconstructed with homogeneous ASA (geometric) and heterogeneous ASA (HASA, Eq. 6). + +| Aperture | Geo (uncorrected) | HASA (corrected) | Paper HASA (Table II) | +|----------|--------------------|-------------------|-----------------------| +| 50 mm | 10.1 ± 8.6 mm | 1.1 ± 1.1 mm | 1.2 ± 0.7 mm | +| 75 mm | 6.9 ± 6.0 mm | 0.7 ± 0.5 mm | 0.9 ± 0.5 mm | +| 100 mm | 5.4 ± 4.8 mm | 0.6 ± 0.4 mm | 0.8 ± 0.4 mm | + +HASA errors are within ~0.2 mm of the paper across all apertures. +Geometric errors are larger than the paper (3.7/2.5/3.5 mm), consistent with using a +single CT slice rather than an average over many skull positions. diff --git a/validation/2D_PAM_Accuracy/results_20260509_114048_fig7.pdf b/validation/2D_PAM_Accuracy/results_20260509_114048_fig7.pdf new file mode 100644 index 0000000..1d178bd Binary files /dev/null and b/validation/2D_PAM_Accuracy/results_20260509_114048_fig7.pdf differ diff --git a/validation/2D_PAM_Accuracy/run_validation.jl b/validation/2D_PAM_Accuracy/run_validation.jl new file mode 100644 index 0000000..25b432b --- /dev/null +++ b/validation/2D_PAM_Accuracy/run_validation.jl @@ -0,0 +1,240 @@ +""" +Reproduce Fig. 7 / Table II of Schoen & Arvanitis (2020) IEEE TMI. + +Point sources on a rectangular grid (axial 30–80 mm, lateral ±20 mm) are +simulated through a human skull with k-Wave. The PAM is reconstructed with +the homogeneous ASA (geometric) and the heterogeneous ASA (HASA, Eq. 6). +Localization errors are tabulated for three receiver apertures (50, 75, 100 mm). + +Each source is simulated once with the largest aperture; smaller apertures are +obtained by zeroing RF outside the requested element range, so reconstruction +is free. + +Run from the project root: + julia --project=. validation/2D_PAM_Accuracy/run_validation.jl [--recon-use-gpu] +""" + +using Pkg +Pkg.activate(joinpath(@__DIR__, "..", "..")) + +using TranscranialFUS +using Statistics, JLD2, Printf, Dates + +# ── Configuration ────────────────────────────────────────────────────────────── + +# Grid (matching paper: 200 µm pitch, 40 ns time step) +const DX = 0.2e-3 # m – axial and lateral spacing +const DT = 40e-9 # s +const T_MAX = 100e-6 # s – covers 80 mm source + full 100 mm aperture +const AXIAL_DIM = 100e-3 # m – total axial extent (skull at 20 mm, sources ≤ 80 mm) +const SKULL_DIST = 20e-3 # m – outer skull surface below receiver +const SLICE_INDEX = 250 # CT slice (same default as run_pam.jl) + +# Source grid (paper: 30–80 mm axial, ±20 mm lateral, 1 MHz) +const FREQ = 1.0e6 # Hz +const NUM_CYCLES = 10 # source duration = 10 µs at 1 MHz +const AXIAL_MM = [30.0, 40.0, 50.0, 60.0, 70.0, 80.0] # mm +const LATERAL_MM = [-20.0, -15.0, -10.0, -5.0, 0.0, 5.0, 10.0, 15.0, 20.0] # mm + +# Apertures to sweep (paper Fig. 7: 50, 75, 100 mm) +const APERTURES_MM = [50.0, 75.0, 100.0] +const MAX_APT_MM = maximum(APERTURES_MM) + +# Reconstruction +const AXIAL_STEP = 50e-6 # m – Δz in HASA marching (matches paper) +const BANDWIDTH = 0.0 # Hz – exact bin (no smoothing) + +# k-Wave simulation always uses GPU. +# PAM reconstruction uses GPU only when --recon-use-gpu is passed. +const USE_GPU_RECON = "--recon-use-gpu" in ARGS + +# ── Domain and medium ────────────────────────────────────────────────────────── + +# Transverse domain must fit the largest aperture plus PML guard. +# PML guard at DX = 0.2 mm: default is max(4, round(4 mm / DX)) = 20 cells = 4 mm. +# Use 12 mm margin on each side → total = MAX_APT_MM + 24 mm. +const TRANS_DIM = (MAX_APT_MM + 24.0) * 1e-3 + +sim_cfg = PAMConfig( + dx = DX, + dz = DX, + axial_dim = AXIAL_DIM, + transverse_dim = TRANS_DIM, + receiver_aperture = MAX_APT_MM * 1e-3, + t_max = T_MAX, + dt = DT, +) + +println("Domain: $(round(Int, AXIAL_DIM*1e3)) mm axial × $(round(Int, TRANS_DIM*1e3)) mm lateral") +println("Grid : $(pam_Nx(sim_cfg)) × $(pam_Ny(sim_cfg)) cells, Nt = $(pam_Nt(sim_cfg))") +println("Active receiver: $(round(Int, MAX_APT_MM)) mm ($(round(Int, MAX_APT_MM*1e-3/DX)) elements)") +println() + +println("Building skull medium (CT slice $SLICE_INDEX, skull at $(round(Int,SKULL_DIST*1e3)) mm)…") +c, rho, med_info = make_pam_medium( + sim_cfg; + aberrator = :skull, + skull_to_transducer = SKULL_DIST, + slice_index = SLICE_INDEX, +) +println(" outer skull row : $(med_info[:outer_row])") +println(" inner skull row : $(med_info[:inner_row])") +println(" c₀ (water) : $(sim_cfg.c0) m/s") +println() + +# ── Sweep storage ───────────────────────────────────────────────────────────── + +n_ax = length(AXIAL_MM) +n_lat = length(LATERAL_MM) +n_apt = length(APERTURES_MM) +n_total = n_ax * n_lat + +# Shape: [n_apt × n_ax × n_lat] +geo_errors_mm = fill(NaN, n_apt, n_ax, n_lat) +hasa_errors_mm = fill(NaN, n_apt, n_ax, n_lat) +geo_ax_err_mm = fill(NaN, n_apt, n_ax, n_lat) +hasa_ax_err_mm = fill(NaN, n_apt, n_ax, n_lat) +geo_lat_err_mm = fill(NaN, n_apt, n_ax, n_lat) +hasa_lat_err_mm = fill(NaN, n_apt, n_ax, n_lat) + +ny = pam_Ny(sim_cfg) +mid_col = ny ÷ 2 + 1 # center column index + +# Pre-compute active column ranges for each aperture. +# Use start = mid - floor(n/2), end = start + n - 1 so the range always has +# exactly n_elem columns regardless of odd/even. +apt_ranges = Dict{Float64, UnitRange{Int}}() +for apt_mm in APERTURES_MM + n_elem = round(Int, apt_mm * 1e-3 / DX) + half = n_elem ÷ 2 # floor division + astart = mid_col - half + aend = astart + n_elem - 1 + (1 <= astart && aend <= ny) || + error("Aperture $(apt_mm) mm exceeds transverse domain $(round(Int, TRANS_DIM*1e3)) mm.") + apt_ranges[apt_mm] = astart:aend +end + +# ── Main sweep ──────────────────────────────────────────────────────────────── + +t_sweep_start = time() +s_count = 0 + +for (ai, ax_mm) in enumerate(AXIAL_MM), (li, lat_mm) in enumerate(LATERAL_MM) + global s_count += 1 + elapsed = time() - t_sweep_start + eta_s = s_count > 1 ? elapsed / (s_count - 1) * (n_total - s_count + 1) : NaN + eta_str = isnan(eta_s) ? "?" : @sprintf("%.0f", eta_s / 60) + @printf("[%2d/%d] ax=%4.0fmm lat=%+5.0fmm (ETA ~%s min)\n", + s_count, n_total, ax_mm, lat_mm, eta_str) + + src = PointSource2D( + depth = ax_mm * 1e-3, + lateral = lat_mm * 1e-3, + frequency = FREQ, + num_cycles = NUM_CYCLES, + ) + + # Simulate once with full (MAX_APT_MM) aperture + rf_full, kgrid, _ = simulate_point_sources(c, rho, [src], sim_cfg; use_gpu=true) + + for (pi, apt_mm) in enumerate(APERTURES_MM) + # Mask RF to requested aperture by zeroing outside active range + apt_range = apt_ranges[apt_mm] + rf_apt = zeros(Float64, ny, size(rf_full, 2)) + rf_apt[apt_range, :] .= rf_full[apt_range, :] + + # Reconstruct: homogeneous ASA (geo) and heterogeneous ASA (HASA) + pam_geo, _, _ = reconstruct_pam(rf_apt, c, sim_cfg; + corrected = false, + frequencies = [FREQ], + bandwidth = BANDWIDTH, + axial_step = AXIAL_STEP, + use_gpu = USE_GPU_RECON, # --recon-use-gpu flag + ) + pam_hasa, _, _ = reconstruct_pam(rf_apt, c, sim_cfg; + corrected = true, + frequencies = [FREQ], + bandwidth = BANDWIDTH, + axial_step = AXIAL_STEP, + use_gpu = USE_GPU_RECON, # --recon-use-gpu flag + ) + + # Localization error via existing analysis function (1 source → argmax) + stats_geo = analyse_pam_2d(pam_geo, kgrid, sim_cfg, [src]) + stats_hasa = analyse_pam_2d(pam_hasa, kgrid, sim_cfg, [src]) + + geo_errors_mm[pi, ai, li] = only(stats_geo[:radial_errors_mm]) + hasa_errors_mm[pi, ai, li] = only(stats_hasa[:radial_errors_mm]) + geo_ax_err_mm[pi, ai, li] = only(stats_geo[:axial_errors_mm]) + hasa_ax_err_mm[pi, ai, li] = only(stats_hasa[:axial_errors_mm]) + geo_lat_err_mm[pi, ai, li] = only(stats_geo[:lateral_errors_mm]) + hasa_lat_err_mm[pi, ai, li] = only(stats_hasa[:lateral_errors_mm]) + end +end + +total_time_min = (time() - t_sweep_start) / 60 +@printf("\nSweep complete in %.1f minutes.\n\n", total_time_min) + +# ── Print results (Table II format) ─────────────────────────────────────────── + +println("="^72) +println("PAM Localization Error — 2D Skull (Schoen & Arvanitis 2020, Table II)") +println("="^72) +println() +@printf(" Sources: %d axial × %d lateral = %d total\n", n_ax, n_lat, n_total) +@printf(" Frequency: %.1f MHz, Axial step: %.0f µm\n", FREQ/1e6, AXIAL_STEP*1e6) +println() + +println(" Aperture │ Uncorrected (geo) │ Corrected (HASA)") +println(" ─────────┼─────────────────────┼────────────────────") +for (pi, apt_mm) in enumerate(APERTURES_MM) + geo_vals = filter(!isnan, geo_errors_mm[pi, :, :]) + hasa_vals = filter(!isnan, hasa_errors_mm[pi, :, :]) + @printf(" %3.0f mm │ %4.1f ± %4.1f mm │ %4.1f ± %4.1f mm\n", + apt_mm, + mean(geo_vals), std(geo_vals), + mean(hasa_vals), std(hasa_vals)) +end +println() + +println(" Reference (paper, Table II):") +println(" Aperture │ Uncorrected │ Corrected") +println(" ─────────┼──────────────────────┼──────────────────────") +println(" 50 mm │ 3.7 ± 2.2 mm │ 1.2 ± 0.7 mm") +println(" 75 mm │ 2.5 ± 1.7 mm │ 0.9 ± 0.5 mm") +println(" 100 mm │ 3.5 ± 1.9 mm │ 0.8 ± 0.4 mm") +println() + +println(" Per-aperture breakdown (mean |axial| / |lateral| error):") +println(" Aperture │ Method │ |Axial| mm │ |Lateral| mm") +println(" ─────────┼────────┼──────────────┼─────────────") +for (pi, apt_mm) in enumerate(APERTURES_MM) + for (method, ax_err, lat_err) in [ + ("geo ", geo_ax_err_mm, geo_lat_err_mm), + ("hasa", hasa_ax_err_mm, hasa_lat_err_mm), + ] + ax_vals = filter(!isnan, abs.(ax_err[pi, :, :])) + lat_vals = filter(!isnan, abs.(lat_err[pi, :, :])) + @printf(" %3.0f mm │ %-6s │ %5.2f ± %4.2f │ %5.2f ± %4.2f\n", + apt_mm, method, mean(ax_vals), std(ax_vals), mean(lat_vals), std(lat_vals)) + end +end +println() + +# ── Save results ────────────────────────────────────────────────────────────── + +outdir = @__DIR__ +ts = Dates.format(now(), "yyyymmdd_HHMMSS") +outfile = joinpath(outdir, "results_$(ts).jld2") + +jldsave(outfile; + geo_errors_mm, hasa_errors_mm, + geo_ax_err_mm, hasa_ax_err_mm, + geo_lat_err_mm, hasa_lat_err_mm, + AXIAL_MM, LATERAL_MM, APERTURES_MM, + FREQ, AXIAL_STEP, DX, DT, T_MAX, + AXIAL_DIM, TRANS_DIM, SKULL_DIST, SLICE_INDEX, + med_info, + total_time_min, +) +println("Results saved → $outfile") diff --git a/validation/3D_PAM_Accuracy/plot_results.jl b/validation/3D_PAM_Accuracy/plot_results.jl new file mode 100644 index 0000000..8551d7a --- /dev/null +++ b/validation/3D_PAM_Accuracy/plot_results.jl @@ -0,0 +1,211 @@ +""" +Plot 3D PAM localization accuracy from saved validation results. + +Layout mirrors the 2D validation figure: + Row 1 (top) – Geometric ASA (uncorrected) + Row 2 (bottom) – HASA (corrected) + Columns – one per lateral-Z slice + +Each panel: lateral Y on x-axis, axial depth on y-axis (shallow → deep). +Bubble size and colour both encode radial localisation error. + +Usage: + julia --project=. validation/3D_PAM_Accuracy/plot_results.jl [results_file.jld2] +""" + +using Pkg +Pkg.activate(joinpath(@__DIR__, "..", "..")) + +using CairoMakie, JLD2, Statistics, Printf + +# ── Load results ────────────────────────────────────────────────────────────── + +results_dir = @__DIR__ +results_file = if !isempty(ARGS) + ARGS[1] +else + files = filter(f -> startswith(f, "results_") && endswith(f, ".jld2"), readdir(results_dir)) + isempty(files) && error("No results_*.jld2 in $results_dir. Run run_validation.jl first.") + joinpath(results_dir, last(sort(files))) +end +println("Loading: $results_file") + +d = load(results_file) +geo_errors_mm = d["geo_errors_mm"] +hasa_errors_mm = d["hasa_errors_mm"] +AXIAL_MM = d["AXIAL_MM"] +LATERAL_Y_MM = d["LATERAL_Y_MM"] +LATERAL_Z_MM = d["LATERAL_Z_MM"] +APERTURE_MM = d["APERTURE_MM"] +FREQ = d["FREQ"] +DX = d["DX"] +DT = d["DT"] +T_MAX = d["T_MAX"] +AXIAL_DIM = d["AXIAL_DIM"] +TRANS_DIM = d["TRANS_DIM"] +SKULL_DIST = d["SKULL_DIST"] +SLICE_INDEX = d["SLICE_INDEX"] + +# ── Print summary ───────────────────────────────────────────────────────────── + +geo_all = filter(!isnan, geo_errors_mm) +hasa_all = filter(!isnan, hasa_errors_mm) +println() +println(" Method │ Mean ± Std error") +println(" ──────────────┼──────────────────────") +@printf(" Geometric ASA │ %.2f ± %.2f mm (n=%d)\n", mean(geo_all), std(geo_all), length(geo_all)) +@printf(" HASA │ %.2f ± %.2f mm (n=%d)\n", mean(hasa_all), std(hasa_all), length(hasa_all)) +println() + +# ── Error colour / size helpers ─────────────────────────────────────────────── + +MAX_ERR = 6.0 +MIN_MS = 2.5 +MAX_MS = 15.0 +errcmap = :plasma + +err_to_ms(e) = MIN_MS + (MAX_MS - MIN_MS) * clamp(e, 0.0, MAX_ERR) / MAX_ERR + +function plot_error_grid!(ax, errors_slice, axial_mm, lateral_mm) + for (ai, az) in enumerate(axial_mm), (li, lat) in enumerate(lateral_mm) + e = errors_slice[ai, li] + isnan(e) && continue + scatter!(ax, [lat], [az]; + color = clamp(e, 0.0, MAX_ERR), + colorrange = (0.0, MAX_ERR), + colormap = errcmap, + markersize = err_to_ms(e), + strokewidth = 0, + ) + end +end + +ax_lim_dep = (maximum(AXIAL_MM) + 5.0, minimum(AXIAL_MM) - 5.0) + +# ── Slice title colours (teal → green → red, extends with grey if needed) ───── + +_base_colors = [ + RGBf(0.10, 0.65, 0.70), + RGBf(0.15, 0.60, 0.15), + RGBf(0.80, 0.20, 0.15), +] +n_slices = length(LATERAL_Z_MM) +slice_colors = [_base_colors[mod1(i, length(_base_colors))] for i in 1:n_slices] + +# ── Theme ───────────────────────────────────────────────────────────────────── + +update_theme!( + fontsize = 9, + Axis = ( + xgridvisible = false, + ygridvisible = false, + topspinevisible = false, + rightspinevisible = false, + ), +) + +fig = Figure(size = (300 * n_slices + 100, 420)) + +# ── Error panels ────────────────────────────────────────────────────────────── + +mid_col = ceil(Int, n_slices / 2) + +for (zi, lz_mm) in enumerate(LATERAL_Z_MM) + tcol = slice_colors[zi] + show_xlabel = zi == mid_col + show_ylabel = zi == 1 + + # Row 1: Geometric ASA (uncorrected) + ax_top = Axis(fig[1, zi]; + xlabel = show_xlabel ? "Lateral Y [mm]" : "", + ylabel = show_ylabel ? "Axial Position [mm]" : "", + yreversed = true, + aspect = DataAspect(), + title = "z = $(round(Int, lz_mm)) mm", + titlecolor = tcol, + xticklabelsize = 7, + yticklabelsize = 7, + yticklabelsvisible = show_ylabel, + ) + plot_error_grid!(ax_top, geo_errors_mm[:, :, zi], AXIAL_MM, LATERAL_Y_MM) + ylims!(ax_top, ax_lim_dep...) + + # Row 2: HASA (corrected) + ax_bot = Axis(fig[2, zi]; + xlabel = show_xlabel ? "Lateral Y [mm]" : "", + ylabel = show_ylabel ? "Axial Position [mm]" : "", + yreversed = true, + aspect = DataAspect(), + xticklabelsize = 7, + yticklabelsize = 7, + yticklabelsvisible = show_ylabel, + ) + plot_error_grid!(ax_bot, hasa_errors_mm[:, :, zi], AXIAL_MM, LATERAL_Y_MM) + ylims!(ax_bot, ax_lim_dep...) +end + +# Row title labels +Label(fig[0, 1:n_slices], "ASA"; + fontsize = 11, font = :bold, + color = RGBf(0.45, 0.0, 0.55), + halign = :center, + valign = :bottom, + padding = (0, 0, 0, 4), +) +Label(fig[2, 1:n_slices, Top()], "HASA"; + fontsize = 11, font = :bold, + color = RGBf(0.70, 0.30, 0.05), + halign = :center, + padding = (0, 0, 6, 0), +) + +# ── Shared error colorbar ───────────────────────────────────────────────────── + +Colorbar(fig[1:2, n_slices + 1]; + colormap = errcmap, + colorrange = (0.0, MAX_ERR), + label = "Error [mm]", + ticklabelsize = 7, + labelsize = 8, + width = 12, + ticks = 0:1:Int(MAX_ERR), +) + +# ── Spacing ─────────────────────────────────────────────────────────────────── + +colgap!(fig.layout, 5) +rowgap!(fig.layout, 3) + +# ── Save ────────────────────────────────────────────────────────────────────── + +figfile = replace(results_file, r"\.jld2$" => "_accuracy_slices.png") +save(figfile, fig; px_per_unit = 2) +println("Saved → $figfile") + +# ── Write results.md ────────────────────────────────────────────────────────── + +md_path = joinpath(results_dir, "results.md") +open(md_path, "w") do io + println(io, "# 3D PAM Accuracy Validation\n") + println(io, "**Aperture:** $(round(Int, APERTURE_MM)) mm × $(round(Int, APERTURE_MM)) mm ") + n_src = length(AXIAL_MM) * length(LATERAL_Y_MM) * length(LATERAL_Z_MM) + println(io, "**Sources:** $(length(AXIAL_MM)) axial × $(length(LATERAL_Y_MM)) y × $(length(LATERAL_Z_MM)) z = $n_src total ") + println(io, "**Frequency:** $(FREQ/1e6) MHz ") + println(io, "**Grid:** $(round(Int, DX*1e6)) µm isotropic ") + println(io, "**Skull:** CT slice $SLICE_INDEX, outer surface at $(round(Int, SKULL_DIST*1e3)) mm \n") + println(io, "| Method | Mean error | Std error | n |") + println(io, "|---------------|-----------|-----------|---|") + @printf(io, "| Geometric ASA | %.2f mm | %.2f mm | %d |\n", mean(geo_all), std(geo_all), length(geo_all)) + @printf(io, "| HASA | %.2f mm | %.2f mm | %d |\n", mean(hasa_all), std(hasa_all), length(hasa_all)) + println(io, "\n## Per-depth breakdown\n") + println(io, "| Depth | Geo mean±std | HASA mean±std |") + println(io, "|-------|----------------|----------------|") + for (ai, ax_mm) in enumerate(AXIAL_MM) + gv = filter(!isnan, geo_errors_mm[ai, :, :]) + hv = filter(!isnan, hasa_errors_mm[ai, :, :]) + @printf(io, "| %3.0f mm | %.2f ± %.2f mm | %.2f ± %.2f mm |\n", + ax_mm, mean(gv), std(gv), mean(hv), std(hv)) + end + println(io, "\n*Figure: $(basename(figfile))*") +end +println("Written → $md_path") diff --git a/validation/3D_PAM_Accuracy/results.md b/validation/3D_PAM_Accuracy/results.md new file mode 100644 index 0000000..d8b474a --- /dev/null +++ b/validation/3D_PAM_Accuracy/results.md @@ -0,0 +1,22 @@ +# 3D PAM Accuracy Validation + +**Aperture:** 64 mm × 64 mm +**Sources:** 3 axial × 3 y × 3 z = 27 total +**Frequency:** 1.0 MHz +**Grid:** 200 µm isotropic +**Skull:** CT slice 250, outer surface at 20 mm + +| Method | Mean error | Std error | n | +|---------------|-----------|-----------|---| +| Geometric ASA | 1.77 mm | 0.88 mm | 27 | +| HASA | 0.49 mm | 0.23 mm | 27 | + +## Per-depth breakdown + +| Depth | Geo mean±std | HASA mean±std | +|-------|----------------|----------------| +| 40 mm | 1.51 ± 0.95 mm | 0.68 ± 0.20 mm | +| 50 mm | 1.82 ± 0.90 mm | 0.47 ± 0.13 mm | +| 60 mm | 1.98 ± 0.84 mm | 0.30 ± 0.17 mm | + +*Figure: results_20260509_184401_accuracy_slices.png* diff --git a/validation/3D_PAM_Accuracy/results_20260509_184401_accuracy_slices.png b/validation/3D_PAM_Accuracy/results_20260509_184401_accuracy_slices.png new file mode 100644 index 0000000..5c3d719 Binary files /dev/null and b/validation/3D_PAM_Accuracy/results_20260509_184401_accuracy_slices.png differ diff --git a/validation/3D_PAM_Accuracy/run_validation.jl b/validation/3D_PAM_Accuracy/run_validation.jl new file mode 100644 index 0000000..43bc486 --- /dev/null +++ b/validation/3D_PAM_Accuracy/run_validation.jl @@ -0,0 +1,252 @@ +""" +3D PAM localization accuracy — single fixed aperture. + +27 point sources (3 axial × 3 lateral-y × 3 lateral-z) are simulated through +a skull CT extruded to 3D with k-Wave. PAM is reconstructed with geometric +ASA and HASA for a fixed 64 mm square aperture. Radial localization errors +and GPU reconstruction timings are recorded. + +Run from the project root: + julia --project=. validation/3D_PAM_Accuracy/run_validation.jl +""" + +using Pkg +Pkg.activate(joinpath(@__DIR__, "..", "..")) + +using TranscranialFUS +using Statistics, JLD2, Printf, Dates + +# ── Configuration ────────────────────────────────────────────────────────────── + +const FREQ = 1.0e6 # Hz +const NUM_CYCLES = 10.0 # source duration = 10 µs at 1 MHz +const DX = 0.2e-3 # m — isotropic grid spacing +const DT = 40e-9 # s +const T_MAX = 120e-6 # s — covers deepest source + diagonal aperture path +const AXIAL_STEP = 50e-6 # m — HASA march step (matches 2D validation) +const BANDWIDTH = 0.0 # Hz — exact bin + +const SKULL_DIST = 20e-3 # m — outer skull surface to receiver +const SLICE_INDEX = 250 # CT slice (same as 2D validation) + +const APERTURE_MM = 64.0 # mm — fixed square aperture in y and z + +# Source grid: 3 × 3 × 3 = 27 sources +const AXIAL_MM = [40.0, 50.0, 60.0] +const LATERAL_Y_MM = [-10.0, 0.0, 10.0] +const LATERAL_Z_MM = [-10.0, 0.0, 10.0] + +# Transverse domain equals the aperture; k-Wave adds PML outside (pml_inside=false) +const TRANS_DIM = APERTURE_MM * 1e-3 # 64 mm + +# Axial domain: skull at 20 mm + deepest source at 60 mm + 10 mm margin +const AXIAL_DIM = 70e-3 # m + +# ── Domain and medium ────────────────────────────────────────────────────────── + +sim_cfg = PAMConfig3D( + dx = DX, + dy = DX, + dz = DX, + axial_dim = AXIAL_DIM, + transverse_dim_y = TRANS_DIM, + transverse_dim_z = TRANS_DIM, + dt = DT, + t_max = T_MAX, + receiver_aperture_y = APERTURE_MM * 1e-3, + receiver_aperture_z = APERTURE_MM * 1e-3, +) + +println("Domain: $(round(Int, AXIAL_DIM*1e3)) mm axial × $(round(Int, TRANS_DIM*1e3)) mm lateral (y & z)") +println("Grid : $(pam_Nx(sim_cfg))×$(pam_Ny(sim_cfg))×$(pam_Nz(sim_cfg)) cells, Nt=$(pam_Nt(sim_cfg))") +println("Aperture: $(round(Int, APERTURE_MM)) mm × $(round(Int, APERTURE_MM)) mm") +println() + +println("Building 3D skull medium (CT slice $SLICE_INDEX, skull at $(round(Int, SKULL_DIST*1e3)) mm)…") +c, rho, med_info = make_pam_medium_3d( + sim_cfg; + aberrator = :skull, + skull_to_transducer = SKULL_DIST, + slice_index_z = SLICE_INDEX, +) +println(" outer skull row : $(med_info[:outer_row])") +println(" inner skull row : $(med_info[:inner_row])") +println(" c₀ (water) : $(sim_cfg.c0) m/s") +println() + +# ── Aperture mask ───────────────────────────────────────────────────────────── + +# The simulation always captures the full ny×nz receiver plane. Restrict to the +# requested aperture by zeroing RF outside the active element range before +# passing to reconstruction, exactly as the 2D validation does. +apt_range_y = receiver_col_range_y(sim_cfg) +apt_range_z = receiver_col_range_z(sim_cfg) +println("Active receiver columns: y=$(apt_range_y), z=$(apt_range_z)") +println() + +# ── Storage ──────────────────────────────────────────────────────────────────── + +n_ax = length(AXIAL_MM) +n_y = length(LATERAL_Y_MM) +n_z = length(LATERAL_Z_MM) + +geo_errors_mm = fill(NaN, n_ax, n_y, n_z) +hasa_errors_mm = fill(NaN, n_ax, n_y, n_z) +geo_recon_ms = fill(NaN, n_ax, n_y, n_z) +hasa_recon_ms = fill(NaN, n_ax, n_y, n_z) +sim_time_s = fill(NaN, n_ax, n_y, n_z) + +# Progress file: written after every source so a crash loses at most one run. +const PROGRESS_FILE = joinpath(@__DIR__, "progress.jld2") + +# Resume from a previous partial run if the progress file exists. +if isfile(PROGRESS_FILE) + prev = load(PROGRESS_FILE) + geo_errors_mm .= prev["geo_errors_mm"] + hasa_errors_mm .= prev["hasa_errors_mm"] + geo_recon_ms .= prev["geo_recon_ms"] + hasa_recon_ms .= prev["hasa_recon_ms"] + sim_time_s .= prev["sim_time_s"] + n_done = count(!isnan, geo_errors_mm) + println("Resuming from progress.jld2 — $n_done/$(n_ax*n_y*n_z) sources already done.") + println() +end + +function save_progress(;total_elapsed_min=NaN) + jldsave(PROGRESS_FILE; + geo_errors_mm, hasa_errors_mm, + geo_recon_ms, hasa_recon_ms, + sim_time_s, + AXIAL_MM, LATERAL_Y_MM, LATERAL_Z_MM, + APERTURE_MM, FREQ, NUM_CYCLES, AXIAL_STEP, + DX, DT, T_MAX, AXIAL_DIM, TRANS_DIM, + SKULL_DIST, SLICE_INDEX, + med_info, + total_time_min=total_elapsed_min, + ) +end + +# ── Main sweep ──────────────────────────────────────────────────────────────── + +t_sweep_start = time() +n_total = n_ax * n_y * n_z +s_count = 0 # index across all sources (including already-done ones) +n_run = 0 # sources actually simulated this session (for ETA) + +for (ai, ax_mm) in enumerate(AXIAL_MM), + (yi, ly_mm) in enumerate(LATERAL_Y_MM), + (zi, lz_mm) in enumerate(LATERAL_Z_MM) + + global s_count += 1 + + if !isnan(geo_errors_mm[ai, yi, zi]) + @printf("[%2d/%d] ax=%4.0fmm ly=%+5.1fmm lz=%+5.1fmm [skipped — already done]\n", + s_count, n_total, ax_mm, ly_mm, lz_mm) + continue + end + + n_remaining = count(isnan, geo_errors_mm) + elapsed = time() - t_sweep_start + eta_s = n_run > 0 ? elapsed / n_run * (n_remaining - 1) : NaN + eta_str = isnan(eta_s) ? "?" : @sprintf("%.0f", eta_s / 60) + @printf("[%2d/%d] ax=%4.0fmm ly=%+5.1fmm lz=%+5.1fmm (ETA ~%s min)\n", + s_count, n_total, ax_mm, ly_mm, lz_mm, eta_str) + + src = PointSource3D( + depth = ax_mm * 1e-3, + lateral_y = ly_mm * 1e-3, + lateral_z = lz_mm * 1e-3, + frequency = FREQ, + num_cycles = NUM_CYCLES, + ) + + # 1. k-Wave simulation (GPU) + t_sim_start = time() + rf_full, kgrid, _ = simulate_point_sources_3d(c, rho, [src], sim_cfg; use_gpu=true) + sim_time_s[ai, yi, zi] = time() - t_sim_start + + # Apply aperture mask: zero RF outside the 64 mm active element window + rf = zeros(eltype(rf_full), size(rf_full)) + rf[apt_range_y, apt_range_z, :] .= rf_full[apt_range_y, apt_range_z, :] + rf_full = nothing # free memory + + # 2. Geometric ASA (corrected=false, GPU) + I_geo, _, geo_info = reconstruct_pam_3d( + rf, c, sim_cfg; + frequencies = [FREQ], + bandwidth = BANDWIDTH, + corrected = false, + axial_step = AXIAL_STEP, + use_gpu = true, + show_progress = false, + ) + geo_recon_ms[ai, yi, zi] = geo_info[:gpu_timing][:march_wall_s] * 1e3 + stats_geo = analyse_pam_3d(I_geo, kgrid, sim_cfg, [src]) + geo_errors_mm[ai, yi, zi] = only(stats_geo[:radial_errors_mm]) + + # 3. HASA (corrected=true, GPU) + I_hasa, _, hasa_info = reconstruct_pam_3d( + rf, c, sim_cfg; + frequencies = [FREQ], + bandwidth = BANDWIDTH, + corrected = true, + axial_step = AXIAL_STEP, + use_gpu = true, + show_progress = false, + ) + hasa_recon_ms[ai, yi, zi] = hasa_info[:gpu_timing][:march_wall_s] * 1e3 + stats_hasa = analyse_pam_3d(I_hasa, kgrid, sim_cfg, [src]) + hasa_errors_mm[ai, yi, zi] = only(stats_hasa[:radial_errors_mm]) + + @printf(" geo=%.2f mm hasa=%.2f mm (geo %.0f ms / hasa %.0f ms GPU march)\n", + geo_errors_mm[ai, yi, zi], hasa_errors_mm[ai, yi, zi], + geo_recon_ms[ai, yi, zi], hasa_recon_ms[ai, yi, zi]) + + global n_run += 1 + n_done = n_total - count(isnan, geo_errors_mm) + save_progress() + @printf(" [progress saved — %d/%d done]\n", n_done, n_total) +end + +total_time_min = (time() - t_sweep_start) / 60 +@printf("\nSweep complete in %.1f minutes.\n\n", total_time_min) + +# ── Print summary ───────────────────────────────────────────────────────────── + +println("="^68) +println("3D PAM Localization Error — Fixed $(round(Int, APERTURE_MM)) mm Aperture") +println("="^68) +println() +@printf(" Sources: %d ax × %d y × %d z = %d total\n", n_ax, n_y, n_z, n_total) +@printf(" Frequency: %.1f MHz, Axial step: %.0f µm\n", FREQ/1e6, AXIAL_STEP*1e6) +println() + +geo_all = filter(!isnan, geo_errors_mm) +hasa_all = filter(!isnan, hasa_errors_mm) +@printf(" Geometric ASA : %.2f ± %.2f mm (mean ± std, n=%d)\n", + mean(geo_all), std(geo_all), length(geo_all)) +@printf(" HASA : %.2f ± %.2f mm (mean ± std, n=%d)\n", + mean(hasa_all), std(hasa_all), length(hasa_all)) +println() +println(" Per-depth breakdown:") +println(" Depth │ Geo error mm │ HASA error mm │ Geo ms │ HASA ms") +println(" ──────┼────────────────┼────────────────┼────────┼────────") +for (ai, ax_mm) in enumerate(AXIAL_MM) + gv = filter(!isnan, geo_errors_mm[ai, :, :]) + hv = filter(!isnan, hasa_errors_mm[ai, :, :]) + gm = filter(!isnan, geo_recon_ms[ai, :, :]) + hm = filter(!isnan, hasa_recon_ms[ai, :, :]) + @printf(" %3.0fmm │ %4.2f ± %4.2f mm │ %4.2f ± %4.2f mm │ %6.1f │ %6.1f\n", + ax_mm, mean(gv), std(gv), mean(hv), std(hv), mean(gm), mean(hm)) +end +println() + +# ── Save results ────────────────────────────────────────────────────────────── + +outdir = @__DIR__ +ts = Dates.format(now(), "yyyymmdd_HHMMSS") +outfile = joinpath(outdir, "results_$(ts).jld2") + +save_progress(total_elapsed_min=total_time_min) +mv(PROGRESS_FILE, outfile; force=true) +println("Results saved → $outfile") diff --git a/validation/3D_Scalability/plot_results.jl b/validation/3D_Scalability/plot_results.jl new file mode 100644 index 0000000..2306121 --- /dev/null +++ b/validation/3D_Scalability/plot_results.jl @@ -0,0 +1,136 @@ +""" +Plot 3D HASA GPU scalability results. + +Two-panel figure: + (a) Wall-clock time vs aperture — one curve per depth, x-axis annotated + with Ny×Nz voxels per plane (the FFT/memory-bandwidth bottleneck). + (b) Wall-clock time vs reconstruction depth — one curve per aperture, + showing the linear growth from increased marching-step count. + +Usage: + julia --project=. validation/3D_Scalability/plot_results.jl [results_file.jld2] +""" + +using Pkg +Pkg.activate(joinpath(@__DIR__, "..", "..")) + +using CairoMakie, JLD2, Printf + +# ── Load ────────────────────────────────────────────────────────────────────── + +results_dir = @__DIR__ +results_file = if !isempty(ARGS) + ARGS[1] +else + files = filter(f -> startswith(f, "results_") && endswith(f, ".jld2"), readdir(results_dir)) + isempty(files) && error("No results_*.jld2 in $results_dir. Run run_scalability.jl first.") + joinpath(results_dir, last(sort(files))) +end +println("Loading: $results_file") + +d = load(results_file) +timing_wall_s = d["timing_wall_s"] # (n_depths × n_apertures) +APERTURES_MM = d["APERTURES_MM"] +DEPTHS_MM = d["DEPTHS_MM"] +DX = d["DX"] +AXIAL_STEP = d["AXIAL_STEP"] + +n_depths = length(DEPTHS_MM) +n_apertures = length(APERTURES_MM) + +# ── Derived labels ───────────────────────────────────────────────────────────── + +Ny_vals = round.(Int, APERTURES_MM .* 1e-3 ./ DX) +vox_k = round.(Int, Ny_vals .^ 2 ./ 1000) +march_steps = round.(Int, DEPTHS_MM .* 1e-3 ./ AXIAL_STEP) + +# Tick labels for aperture axis: "50 mm\n(250×250=62k)" +apt_tick_labels = [@sprintf("%d mm\n(%d×%d=%dk)", round(Int,a), n, n, v) + for (a, n, v) in zip(APERTURES_MM, Ny_vals, vox_k)] + +# Print summary table +println() +println("Wall-clock GPU HASA [s] (rows = depth from skull, cols = aperture)") +@printf(" %12s │", "Depth \\ Apt") +for apt_mm in APERTURES_MM + @printf(" %5.0f mm", apt_mm) +end +println() +for (di, depth_mm) in enumerate(DEPTHS_MM) + @printf(" %8.0f mm │", depth_mm) + for ai in 1:n_apertures + @printf(" %6.2f s", timing_wall_s[di, ai]) + end + println() +end +println() + +# ── Figure ──────────────────────────────────────────────────────────────────── + +update_theme!(fontsize = 9) + +DEPTH_COLORS = [:steelblue, :coral, :forestgreen, :darkorchid, :darkorange] +APT_MARKERS = [:circle, :diamond, :rect, :utriangle] + +fig = Figure(size = (820, 370)) + +# ── (a) Time vs aperture (one curve per depth) ───────────────────────────────── + +ax_l = Axis(fig[1, 1]; + title = "(a) Scaling with transverse grid size", + xlabel = "Aperture [mm] (Ny × Nz voxels/plane)", + ylabel = "GPU HASA wall-clock [s]", + xticks = (APERTURES_MM, apt_tick_labels), + xticklabelsize = 8, +) + +for (di, depth_mm) in enumerate(DEPTHS_MM) + label = @sprintf("%d mm depth", round(Int, depth_mm)) + lines!(ax_l, APERTURES_MM, timing_wall_s[di, :]; + label = label, + color = DEPTH_COLORS[di], + linewidth = 1.5, + ) + scatter!(ax_l, APERTURES_MM, timing_wall_s[di, :]; + color = DEPTH_COLORS[di], + markersize = 7, + ) +end + +axislegend(ax_l; title = "Recon depth\n(below skull)", position = :lt, + labelsize = 8, titlesize = 8) + +# ── (b) Time vs depth (one curve per aperture) ───────────────────────────────── + +ax_r = Axis(fig[1, 2]; + title = "(b) Scaling with reconstruction depth", + xlabel = "Reconstruction depth below skull [mm]", + ylabel = "GPU HASA wall-clock [s]", + xticks = (DEPTHS_MM, string.(round.(Int, DEPTHS_MM))), +) + +for (ai, apt_mm) in enumerate(APERTURES_MM) + Ny = Ny_vals[ai] + label = @sprintf("%d mm (%d×%d = %dk vox)", round(Int,apt_mm), Ny, Ny, vox_k[ai]) + lines!(ax_r, DEPTHS_MM, timing_wall_s[:, ai]; + label = label, + color = DEPTH_COLORS[ai], + linewidth = 1.5, + ) + scatter!(ax_r, DEPTHS_MM, timing_wall_s[:, ai]; + color = DEPTH_COLORS[ai], + marker = APT_MARKERS[ai], + markersize = 7, + ) +end + +axislegend(ax_r; title = "Aperture\n(voxels/plane)", position = :lt, + labelsize = 8, titlesize = 8) + +colgap!(fig.layout, 22) + +# ── Save ────────────────────────────────────────────────────────────────────── + +figfile = replace(results_file, r"\.jld2$" => "_scalability.pdf") +save(figfile, fig) +println("Saved → $figfile") diff --git a/validation/3D_Scalability/results_20260511_122538_scalability.pdf b/validation/3D_Scalability/results_20260511_122538_scalability.pdf new file mode 100644 index 0000000..bc247c0 Binary files /dev/null and b/validation/3D_Scalability/results_20260511_122538_scalability.pdf differ diff --git a/validation/3D_Scalability/results_20260511_140954_scalability.pdf b/validation/3D_Scalability/results_20260511_140954_scalability.pdf new file mode 100644 index 0000000..7199f53 Binary files /dev/null and b/validation/3D_Scalability/results_20260511_140954_scalability.pdf differ diff --git a/validation/3D_Scalability/run_scalability.jl b/validation/3D_Scalability/run_scalability.jl new file mode 100644 index 0000000..56f1b68 --- /dev/null +++ b/validation/3D_Scalability/run_scalability.jl @@ -0,0 +1,332 @@ +""" +3D HASA GPU Scalability Study — reconstruction time vs aperture and depth. + +One k-Wave simulation (64 mm aperture, source at 50 mm — same parameters as +the speed benchmark) provides the base RF data. The reconstruction config is +then swept over all (depth, aperture) combinations to isolate the two +independent GPU compute bottlenecks: + + Transverse grid size Ny×Nz → FFT / memory-bandwidth bottleneck + Axial reconstruction depth → marching-step count, scales linearly + +Aperture sweep uses crop / zero-pad of the base RF data so that no additional +k-Wave simulations are required; only GPU HASA reconstruction is timed. + +Inner-loop structure for a single call (nfreq=1, nwindows=1): + n_march_rows = depth_mm / (AXIAL_STEP*1e3) (linear in depth) + ops per row = 4 substeps (HASA) × 2D FFT on Ny×Nz + +Run from the project root: + julia --project=. validation/3D_Scalability/run_scalability.jl +""" + +using Pkg +Pkg.activate(joinpath(@__DIR__, "..", "..")) + +using TranscranialFUS +using JLD2, Printf, Dates, CUDA + +# ── Fixed parameters (consistent with 3D_PAM_Accuracy / 3D_Speed_Benchmark) ── + +const FREQ = 1.0e6 +const NUM_CYCLES = 10.0 +const DX = 0.2e-3 +const DT = 40e-9 +const T_MAX = 120e-6 +const AXIAL_STEP = 50e-6 +const BANDWIDTH = 0.0 + +const SKULL_DIST = 20e-3 +const SLICE_INDEX = 250 + +# ── Sweep parameters ────────────────────────────────────────────────────────── + +const APERTURES_MM = [25, 50.0, 75.0] # receiver aperture (y and z); 100 mm OOMs on RTX 3080 +const DEPTHS_MM = [20.0, 40.0, 60.0, 80.0, 100.0] # depth below skull surface + +let apts = round.(Int, APERTURES_MM .* 1e-3 ./ DX) + println("Aperture sweep: $(round.(Int, APERTURES_MM)) mm → Ny=Nz = $apts") + println(" voxels/plane : $(apts .^ 2)") +end +let steps = round.(Int, DEPTHS_MM .* 1e-3 ./ AXIAL_STEP) + println("Depth sweep : $(round.(Int, DEPTHS_MM)) mm below skull") + println(" march steps : $steps (×4 substeps for HASA)") +end +println() + +# ── Scalability sweep ───────────────────────────────────────────────────────── + +n_depths = length(DEPTHS_MM) +n_apertures = length(APERTURES_MM) + +timing_wall_s = fill(NaN, n_depths, n_apertures) +timing_march_s = fill(NaN, n_depths, n_apertures) + +# Progress file: written after every point so a crash loses at most one run. +const PROGRESS_FILE = joinpath(@__DIR__, "progress.jld2") + +if isfile(PROGRESS_FILE) + prev = load(PROGRESS_FILE) + prev_apts = prev["APERTURES_MM"] + prev_deps = prev["DEPTHS_MM"] + ai_map = [findfirst(==(a), prev_apts) for a in APERTURES_MM] + di_map = [findfirst(==(d), prev_deps) for d in DEPTHS_MM] + for (di_new, di_old) in enumerate(di_map) + di_old === nothing && continue + for (ai_new, ai_old) in enumerate(ai_map) + ai_old === nothing && continue + timing_wall_s[di_new, ai_new] = prev["timing_wall_s"][di_old, ai_old] + timing_march_s[di_new, ai_new] = prev["timing_march_s"][di_old, ai_old] + end + end + n_done = count(!isnan, timing_wall_s) + println("Resuming from progress.jld2 — $n_done/$(n_depths*n_apertures) points already done.") + println() +end + +# ── RF resize helper (centre crop or zero-pad) ──────────────────────────────── + +function resize_rf(rf::AbstractArray, Ny_target::Int, Nz_target::Int) + Ny_src, Nz_src, Nt = size(rf) + rf_out = zeros(eltype(rf), Ny_target, Nz_target, Nt) + + if Ny_target <= Ny_src + y_src_lo = (Ny_src - Ny_target) ÷ 2 + 1 + y_range_src = y_src_lo:(y_src_lo + Ny_target - 1) + y_range_dst = 1:Ny_target + else + y_dst_lo = (Ny_target - Ny_src) ÷ 2 + 1 + y_range_src = 1:Ny_src + y_range_dst = y_dst_lo:(y_dst_lo + Ny_src - 1) + end + + if Nz_target <= Nz_src + z_src_lo = (Nz_src - Nz_target) ÷ 2 + 1 + z_range_src = z_src_lo:(z_src_lo + Nz_target - 1) + z_range_dst = 1:Nz_target + else + z_dst_lo = (Nz_target - Nz_src) ÷ 2 + 1 + z_range_src = 1:Nz_src + z_range_dst = z_dst_lo:(z_dst_lo + Nz_src - 1) + end + + rf_out[y_range_dst, z_range_dst, :] .= rf[y_range_src, z_range_src, :] + return rf_out +end + +# ── k-Wave simulation (only if there are points left to run) ────────────────── + +const KW_APERTURE = 64e-3 +const KW_AXIAL_DIM = 70e-3 + +rf_base = nothing + +if any(isnan, timing_wall_s) + + kw_cfg = PAMConfig3D( + dx = DX, + dy = DX, + dz = DX, + axial_dim = KW_AXIAL_DIM, + transverse_dim_y = KW_APERTURE, + transverse_dim_z = KW_APERTURE, + dt = DT, + t_max = T_MAX, + receiver_aperture_y = KW_APERTURE, + receiver_aperture_z = KW_APERTURE, + ) + + println("k-Wave grid : $(pam_Nx(kw_cfg))×$(pam_Ny(kw_cfg))×$(pam_Nz(kw_cfg)), Nt=$(pam_Nt(kw_cfg))") + println() + + println("Building skull medium for k-Wave…") + c_kw, rho_kw, _ = make_pam_medium_3d( + kw_cfg; + aberrator = :skull, + skull_to_transducer = SKULL_DIST, + slice_index_z = SLICE_INDEX, + ) + + src = PointSource3D( + depth = 50e-3, + lateral_y = 0.0, + lateral_z = 0.0, + frequency = FREQ, + num_cycles = NUM_CYCLES, + ) + + println("Running k-Wave (GPU)…") + kwave_tmp = mktempdir() + t_sim = @elapsed begin + rf_base, _, _ = simulate_point_sources_3d(c_kw, rho_kw, [src], kw_cfg; + use_gpu=true, kwave_data_path=kwave_tmp) + end + @printf(" k-Wave wall-clock: %.1f s\n\n", t_sim) + GC.gc(true) + rm(kwave_tmp; recursive=true, force=true) + + # GPU warm-up + print("GPU warm-up (small domain) … ") + flush(stdout) + let + cfg_w = PAMConfig3D( + dx = DX, dy = DX, dz = DX, + axial_dim = 30e-3, + transverse_dim_y = 50e-3, + transverse_dim_z = 50e-3, + dt = DT, t_max = T_MAX, + receiver_aperture_y = 50e-3, + receiver_aperture_z = 50e-3, + ) + c_w, _, _ = make_pam_medium_3d(cfg_w; + aberrator=:skull, skull_to_transducer=SKULL_DIST, slice_index_z=SLICE_INDEX) + rf_w = zeros(Float32, pam_Ny(cfg_w), pam_Nz(cfg_w), pam_Nt(cfg_w)) + reconstruct_pam_3d(rf_w, c_w, cfg_w; + frequencies=[FREQ], bandwidth=BANDWIDTH, corrected=true, + axial_step=AXIAL_STEP, use_gpu=true, show_progress=false) + end + println("done.\n") + +else + println("All points already complete — skipping k-Wave and warm-up.\n") +end + +function save_progress() + jldsave(PROGRESS_FILE; + timing_wall_s, timing_march_s, + APERTURES_MM, DEPTHS_MM, + FREQ, NUM_CYCLES, DX, DT, T_MAX, AXIAL_STEP, BANDWIDTH, + SKULL_DIST, SLICE_INDEX, + ) +end + +t_sweep_start = time() +n_total = n_depths * n_apertures +s_count = 0 +n_run = 0 + +for (di, depth_mm) in enumerate(DEPTHS_MM) + + recon_axial_dim = (depth_mm + SKULL_DIST * 1e3) * 1e-3 # skull + depth in metres + + # Build medium for max aperture at this depth; crop for smaller apertures. + cfg_max = PAMConfig3D( + dx = DX, + dy = DX, + dz = DX, + axial_dim = recon_axial_dim, + transverse_dim_y = maximum(APERTURES_MM) * 1e-3, + transverse_dim_z = maximum(APERTURES_MM) * 1e-3, + dt = DT, + t_max = T_MAX, + receiver_aperture_y = maximum(APERTURES_MM) * 1e-3, + receiver_aperture_z = maximum(APERTURES_MM) * 1e-3, + ) + c_max, _, _ = make_pam_medium_3d(cfg_max; + aberrator=:skull, skull_to_transducer=SKULL_DIST, slice_index_z=SLICE_INDEX) + Ny_max = pam_Ny(cfg_max) # 500 + + for (ai, apt_mm) in enumerate(APERTURES_MM) + + global s_count += 1 + + if !isnan(timing_wall_s[di, ai]) + @printf("[%2d/%d] depth=%3.0f mm, aperture=%3.0f mm [skipped — already done]\n", + s_count, n_total, depth_mm, apt_mm) + continue + end + + + n_remaining = count(isnan, timing_wall_s) + elapsed = time() - t_sweep_start + eta_s = n_run > 0 ? elapsed / n_run * (n_remaining - 1) : NaN + eta_str = isnan(eta_s) ? "?" : @sprintf("%.0f", eta_s / 60) + + Ny_tgt = round(Int, apt_mm * 1e-3 / DX) + @printf("[%2d/%d] depth=%3.0f mm, aperture=%3.0f mm (Ny=Nz=%d, %d march rows, ETA ~%s min) … ", + s_count, n_total, depth_mm, apt_mm, Ny_tgt, + round(Int, depth_mm * 1e-3 / AXIAL_STEP), eta_str) + flush(stdout) + + # Reconstruction config for this (depth, aperture) + cfg_recon = PAMConfig3D( + dx = DX, + dy = DX, + dz = DX, + axial_dim = recon_axial_dim, + transverse_dim_y = apt_mm * 1e-3, + transverse_dim_z = apt_mm * 1e-3, + dt = DT, + t_max = T_MAX, + receiver_aperture_y = apt_mm * 1e-3, + receiver_aperture_z = apt_mm * 1e-3, + ) + + # Crop medium from the max-aperture build (centre slice) + y_lo = (Ny_max - Ny_tgt) ÷ 2 + 1 + y_hi = y_lo + Ny_tgt - 1 + c_recon = c_max[:, y_lo:y_hi, y_lo:y_hi] + + # RF resized to match aperture (crop or zero-pad from 64 mm base) + rf_apt = resize_rf(rf_base, Ny_tgt, Ny_tgt) + + # GPU HASA reconstruction + t_wall = @elapsed begin + _, _, info = reconstruct_pam_3d( + rf_apt, c_recon, cfg_recon; + frequencies = [FREQ], + bandwidth = BANDWIDTH, + corrected = true, + axial_step = AXIAL_STEP, + use_gpu = true, + show_progress = false, + ) + timing_march_s[di, ai] = get(get(info, :gpu_timing, Dict()), :march_wall_s, NaN) + end + + timing_wall_s[di, ai] = t_wall + @printf("%.2f s (march %.2f s)\n", t_wall, timing_march_s[di, ai]) + + # Free CPU temporaries and flush GPU memory pool before the next (larger) run. + rf_apt = nothing + c_recon = nothing + GC.gc(true) + CUDA.reclaim() + + global n_run += 1 + n_done = count(!isnan, timing_wall_s) + save_progress() + @printf(" [progress saved — %d/%d done]\n", n_done, n_total) + end +end + +total_min = (time() - t_sweep_start) / 60 +@printf("\nSweep complete in %.1f minutes.\n\n", total_min) + +# ── Summary table ───────────────────────────────────────────────────────────── + +println("Wall-clock GPU HASA [s] (rows = depth from skull, cols = aperture)") +@printf(" %10s │", "Depth \\ Apt") +for apt_mm in APERTURES_MM + @printf(" %5.0f mm", apt_mm) +end +println() +println(" " * "─"^11 * "┼" * "─"^(9 * n_apertures)) +for (di, depth_mm) in enumerate(DEPTHS_MM) + @printf(" %6.0f mm │", depth_mm) + for (ai, _) in enumerate(APERTURES_MM) + @printf(" %7.2f s", timing_wall_s[di, ai]) + end + println() +end +println() + +# ── Save ────────────────────────────────────────────────────────────────────── + +outdir = @__DIR__ +ts = Dates.format(now(), "yyyymmdd_HHMMSS") +outfile = joinpath(outdir, "results_$(ts).jld2") + +save_progress() +mv(PROGRESS_FILE, outfile; force=true) +println("Results saved → $outfile") diff --git a/validation/3D_Speed_Benchmark/README.md b/validation/3D_Speed_Benchmark/README.md new file mode 100644 index 0000000..cc998be --- /dev/null +++ b/validation/3D_Speed_Benchmark/README.md @@ -0,0 +1,11 @@ +# 3D PAM Speed Benchmark + +Wall-clock reconstruction times for a single centre source at 50 mm depth, +64 mm × 64 mm aperture, 0.2 mm grid, CT skull at 20 mm. + +| | CPU | GPU | +|--------|---------|--------| +| **ASA** | 37.1 s | 34.4 s | +| **HASA** | 46.7 s | 9.7 s | + +HASA massively benefits from GPU speedup. But not in a real time regime yet. \ No newline at end of file diff --git a/validation/3D_Speed_Benchmark/plot_results.jl b/validation/3D_Speed_Benchmark/plot_results.jl new file mode 100644 index 0000000..642781a --- /dev/null +++ b/validation/3D_Speed_Benchmark/plot_results.jl @@ -0,0 +1,160 @@ +""" +Plot 3D PAM speed benchmark results. + +Produces a figure with: + - 2×2 grid: Y-Z cross-section at source depth for each mode + (CPU ASA | GPU ASA) / (CPU HASA | GPU HASA) + - Bar chart: wall-clock reconstruction time per mode (log scale) + +Usage: + julia --project=. validation/3D_Speed_Benchmark/plot_results.jl [results_file.jld2] +""" + +using Pkg +Pkg.activate(joinpath(@__DIR__, "..", "..")) + +using CairoMakie, JLD2, TranscranialFUS, Printf + +# ── Load ────────────────────────────────────────────────────────────────────── + +results_dir = @__DIR__ +results_file = if !isempty(ARGS) + ARGS[1] +else + files = filter(f -> startswith(f, "results_") && endswith(f, ".jld2"), readdir(results_dir)) + isempty(files) && error("No results_*.jld2 in $results_dir. Run run_benchmark.jl first.") + joinpath(results_dir, last(sort(files))) +end +println("Loading: $results_file") + +d = load(results_file) +intensities = d["intensities"] +wall_times = d["wall_times"] +DX = d["DX"] +DT = d["DT"] +T_MAX = d["T_MAX"] +APERTURE = d["APERTURE"] +AXIAL_DIM = d["AXIAL_DIM"] +TRANS_DIM = d["TRANS_DIM"] +SKULL_DIST = d["SKULL_DIST"] +SLICE_INDEX = d["SLICE_INDEX"] +FREQ = d["FREQ"] +NUM_CYCLES = d["NUM_CYCLES"] + +# ── Rebuild config for coordinate arrays ────────────────────────────────────── + +sim_cfg = PAMConfig3D( + dx = DX, + dy = DX, + dz = DX, + axial_dim = AXIAL_DIM, + transverse_dim_y = TRANS_DIM, + transverse_dim_z = TRANS_DIM, + dt = DT, + t_max = T_MAX, + receiver_aperture_y = APERTURE, + receiver_aperture_z = APERTURE, +) + +rr = receiver_row(sim_cfg) +Ny = pam_Ny(sim_cfg) +Nz = pam_Nz(sim_cfg) +dx_mm = DX * 1e3 + +y_coords = [(j - (Ny ÷ 2 + 1)) * dx_mm for j in 1:Ny] +z_coords = [(k - (Nz ÷ 2 + 1)) * dx_mm for k in 1:Nz] + +# Source is at 50 mm depth from receiver +src_row = rr + round(Int, 50e-3 / DX) +src_row = clamp(src_row, 1, pam_Nx(sim_cfg)) + +# ── Print summary ───────────────────────────────────────────────────────────── + +LABELS = ["CPU_ASA", "GPU_ASA", "CPU_HASA", "GPU_HASA"] + +println() +println(" Mode │ Wall-clock") +println(" ───────────┼───────────") +for lbl in LABELS + @printf(" %-10s │ %7.2f s\n", lbl, wall_times[lbl]) +end +println() + +# ── Figure ──────────────────────────────────────────────────────────────────── + +update_theme!(fontsize = 9) + +fig = Figure(size = (900, 720)) + +PANEL_LABELS = ["CPU — Geometric ASA", "GPU — Geometric ASA", + "CPU — HASA", "GPU — HASA"] +PANEL_KEYS = ["CPU_ASA", "GPU_ASA", "CPU_HASA", "GPU_HASA"] +POSITIONS = [(1, 1), (1, 2), (2, 1), (2, 2)] + +for (lbl, title, pos) in zip(PANEL_KEYS, PANEL_LABELS, POSITIONS) + I = intensities[lbl] + slice = I[src_row, :, :] # (Ny, Nz) — Y-Z plane at source depth + I_max = max(maximum(slice), 1f-30) + slice_db = 20f0 .* log10.(slice ./ I_max .+ 1f-10) + + ax = Axis(fig[pos[1], pos[2]]; + title = @sprintf("%s\n%.2f s", title, wall_times[lbl]), + xlabel = "Lateral Z [mm]", + ylabel = "Lateral Y [mm]", + aspect = DataAspect(), + ) + heatmap!(ax, z_coords, y_coords, slice_db'; + colorrange = (-40, 0), + colormap = :inferno, + ) +end + +# Shared colorbar +Colorbar(fig[1:2, 3]; + colorrange = (-40, 0), + colormap = :inferno, + label = "Normalised intensity [dB]", + width = 14, + ticklabelsize = 8, + labelsize = 9, + ticks = -40:10:0, +) + +# Timing bar chart +ax_bar = Axis(fig[3, 1:2]; + title = "Reconstruction wall-clock time", + ylabel = "Time [s]", + yscale = log10, + xticks = (1:4, LABELS), + xticklabelsize = 9, +) + +bar_vals = [wall_times[l] for l in LABELS] +bar_colors = [:steelblue, :coral, :steelblue, :coral] +barplot!(ax_bar, 1:4, bar_vals; color = bar_colors, strokewidth = 0.5, strokecolor = :black) + +# Annotate each bar with its value +for (i, v) in enumerate(bar_vals) + text!(ax_bar, i, v * 1.3; + text = @sprintf("%.1f s", v), + align = (:center, :bottom), + fontsize = 8, + ) +end + +Legend(fig[3, 3], + [PolyElement(color=:steelblue), PolyElement(color=:coral)], + ["ASA", "HASA"]; + framevisible = false, + labelsize = 9, +) + +rowgap!(fig.layout, 10) +colgap!(fig.layout, 8) +rowsize!(fig.layout, 3, Relative(0.28)) + +# ── Save ────────────────────────────────────────────────────────────────────── + +figfile = replace(results_file, r"\.jld2$" => "_speed_benchmark.pdf") +save(figfile, fig) +println("Saved → $figfile") diff --git a/validation/3D_Speed_Benchmark/results_20260509_190248_speed_benchmark.pdf b/validation/3D_Speed_Benchmark/results_20260509_190248_speed_benchmark.pdf new file mode 100644 index 0000000..bf4a7da Binary files /dev/null and b/validation/3D_Speed_Benchmark/results_20260509_190248_speed_benchmark.pdf differ diff --git a/validation/3D_Speed_Benchmark/run_benchmark.jl b/validation/3D_Speed_Benchmark/run_benchmark.jl new file mode 100644 index 0000000..8067e2b --- /dev/null +++ b/validation/3D_Speed_Benchmark/run_benchmark.jl @@ -0,0 +1,162 @@ +""" +3D HASA Speed Benchmark — CPU vs GPU, ASA vs HASA. + +A single centre source (50 mm depth, lateral = 0) is simulated once with +k-Wave (GPU). The RF data is then reconstructed with all four mode +combinations and wall-clock times are recorded for direct comparison: + CPU ASA | CPU HASA + GPU ASA | GPU HASA + +Parameters match validation/3D_PAM_Accuracy/run_validation.jl exactly. + +Run from the project root: + julia --project=. validation/3D_Speed_Benchmark/run_benchmark.jl +""" + +using Pkg +Pkg.activate(joinpath(@__DIR__, "..", "..")) + +using TranscranialFUS +using JLD2, Printf, Dates + +# ── Configuration (identical to 3D_PAM_Accuracy) ────────────────────────────── + +const FREQ = 1.0e6 +const NUM_CYCLES = 10.0 +const DX = 0.2e-3 +const DT = 40e-9 +const T_MAX = 120e-6 +const AXIAL_STEP = 50e-6 +const BANDWIDTH = 0.0 + +const SKULL_DIST = 20e-3 +const SLICE_INDEX = 250 +const APERTURE = 64e-3 + +const AXIAL_DIM = 70e-3 +const TRANS_DIM = APERTURE + +# ── Domain and medium ───────────────────────────────────────────────────────── + +sim_cfg = PAMConfig3D( + dx = DX, + dy = DX, + dz = DX, + axial_dim = AXIAL_DIM, + transverse_dim_y = TRANS_DIM, + transverse_dim_z = TRANS_DIM, + dt = DT, + t_max = T_MAX, + receiver_aperture_y = APERTURE, + receiver_aperture_z = APERTURE, +) + +println("Domain : $(round(Int, AXIAL_DIM*1e3)) mm axial × $(round(Int, TRANS_DIM*1e3)) mm transverse") +println("Grid : $(pam_Nx(sim_cfg))×$(pam_Ny(sim_cfg))×$(pam_Nz(sim_cfg)) cells, Nt=$(pam_Nt(sim_cfg))") +println("Aperture: $(round(Int, APERTURE*1e3)) mm × $(round(Int, APERTURE*1e3)) mm") +println() + +println("Building skull medium (CT slice $SLICE_INDEX, skull at $(round(Int, SKULL_DIST*1e3)) mm)…") +c, rho, med_info = make_pam_medium_3d( + sim_cfg; + aberrator = :skull, + skull_to_transducer = SKULL_DIST, + slice_index_z = SLICE_INDEX, +) +println(" outer skull row : $(med_info[:outer_row])") +println(" inner skull row : $(med_info[:inner_row])") +println() + +# ── Single centre source ─────────────────────────────────────────────────────── + +src = PointSource3D( + depth = 50e-3, + lateral_y = 0.0, + lateral_z = 0.0, + frequency = FREQ, + num_cycles = NUM_CYCLES, +) + +# ── k-Wave simulation ───────────────────────────────────────────────────────── + +println("Running k-Wave (GPU)…") +t_sim = @elapsed begin + rf_full, kgrid, _ = simulate_point_sources_3d(c, rho, [src], sim_cfg; use_gpu=true) +end +@printf(" k-Wave wall-clock : %.1f s\n\n", t_sim) + +apt_range_y = receiver_col_range_y(sim_cfg) +apt_range_z = receiver_col_range_z(sim_cfg) +rf = zeros(eltype(rf_full), size(rf_full)) +rf[apt_range_y, apt_range_z, :] .= rf_full[apt_range_y, apt_range_z, :] +rf_full = nothing + +# ── Four reconstructions ────────────────────────────────────────────────────── + +const MODES = [ + (use_gpu=false, corrected=false, label="CPU_ASA"), + (use_gpu=false, corrected=true, label="CPU_HASA"), + (use_gpu=true, corrected=false, label="GPU_ASA"), + (use_gpu=true, corrected=true, label="GPU_HASA"), +] + +intensities = Dict{String,Array{Float32,3}}() +info_dicts = Dict{String,Any}() +wall_times = Dict{String,Float64}() + +println("Reconstructions:") +for m in MODES + @printf(" %-10s … ", m.label) + flush(stdout) + t = @elapsed begin + I, _, info = reconstruct_pam_3d( + rf, c, sim_cfg; + frequencies = [FREQ], + bandwidth = BANDWIDTH, + corrected = m.corrected, + axial_step = AXIAL_STEP, + use_gpu = m.use_gpu, + show_progress = false, + ) + intensities[m.label] = I + info_dicts[m.label] = info + end + wall_times[m.label] = t + @printf("%.2f s\n", t) +end + +# ── Summary ─────────────────────────────────────────────────────────────────── + +println() +println("="^52) +println("3D PAM Speed Benchmark — $(round(Int, APERTURE*1e3)) mm aperture, source at 50 mm") +println("="^52) +println() +println(" Mode │ Wall-clock") +println(" ───────────┼───────────") +for m in MODES + @printf(" %-10s │ %7.2f s\n", m.label, wall_times[m.label]) +end +println() +@printf(" GPU/CPU speedup (HASA): %.1f×\n", + wall_times["CPU_HASA"] / wall_times["GPU_HASA"]) +@printf(" GPU/CPU speedup (ASA) : %.1f×\n", + wall_times["CPU_ASA"] / wall_times["GPU_ASA"]) +println() + +# ── Save ────────────────────────────────────────────────────────────────────── + +outdir = @__DIR__ +ts = Dates.format(now(), "yyyymmdd_HHMMSS") +outfile = joinpath(outdir, "results_$(ts).jld2") + +jldsave(outfile; + intensities, + wall_times, + info_dicts, + sim_time_s = t_sim, + FREQ, NUM_CYCLES, DX, DT, T_MAX, AXIAL_STEP, BANDWIDTH, + AXIAL_DIM, TRANS_DIM, APERTURE, SKULL_DIST, SLICE_INDEX, + med_info, +) +println("Results saved → $outfile") diff --git a/visualisation/README.md b/visualisation/README.md new file mode 100644 index 0000000..65d86cb --- /dev/null +++ b/visualisation/README.md @@ -0,0 +1,94 @@ +# PAM Window Convergence Visualisation + +This folder contains a self-contained 3D PAM animation pipeline. It generates a +synthetic vascular-network source distribution, simulates RF data, reconstructs +the case window by window with HASA, chooses the final source-F1-optimal +threshold, and renders a fixed-threshold convergence movie. + +## Run + +```powershell +julia --project=. visualisation/make_window_convergence.jl +``` + +Default case: + +- 3D network source at `45:0:0 mm` +- CT skull aberrator, `slice-index=250`, skull distance `20 mm` +- `dx=0.2 mm`, `dy=0.2 mm`, `dz=0.2 mm` +- `t_max=300 us` +- `40 us` windows, `20 us` hop +- `40 kHz` reconstruction bandwidth +- random source phases resampled per reconstruction window +- GPU k-Wave and GPU reconstruction + +The default run is intentionally heavy. A quick water smoke test is: + +```powershell +julia --project=. visualisation/make_window_convergence.jl --aberrator=none --max-windows=2 +``` + +Check the configuration without running simulation/reconstruction: + +```powershell +julia --project=. visualisation/make_window_convergence.jl --dry-run=true +``` + +## Outputs + +Outputs are written to: + +```text +visualisation/outputs/_window_convergence/ +``` + +The folder contains: + +- `data.jld2`: RF data, medium arrays, final cumulative volume, truth mask, + threshold search results, selected windows, and per-frame metrics +- `summary.json`: human-readable run summary +- `frames/frame_0001.png`, etc. +- `pam_window_convergence.mp4` + +## Threshold Story + +The script first reconstructs all selected windows and computes the final +cumulative HASA volume. It searches thresholds from `0.10:0.01:0.95`, chooses +the threshold with the best final source F1, and freezes the corresponding +absolute intensity cutoff for every frame. + +Each frame therefore uses the same detection boundary. The orange region shows +the cumulative prediction above that fixed cutoff, white contours show the truth +mask, blue contours show the current-window contribution, and source points are +colored by detected versus not-yet-detected. + +## ffmpeg + +Install `ffmpeg` and make sure it is available on PATH to create the MP4. If it +is missing, the script still writes all PNG frames and prints the exact encoding +command. + +Useful options: + +```powershell +julia --project=. visualisation/make_window_convergence.jl ` + --out-dir=visualisation/outputs/test_run ` + --t-max-us=300 ` + --dy-mm=0.2 ` + --dz-mm=0.2 ` + --max-windows=0 ` + --fps=12 ` + --frames-only=false +``` + +To reuse an existing k-Wave simulation and medium cache: + +```powershell +julia --project=. visualisation/make_window_convergence.jl ` + --from-data=visualisation/outputs/_window_convergence/data.jld2 ` + --out-dir=visualisation/outputs/rerender_attempt +``` + +`--from-data` skips medium construction and k-Wave RF simulation. It still +reruns the window reconstructions so you can adjust rendering and threshold +logic without paying the forward-simulation cost again. diff --git a/visualisation/make_window_convergence.jl b/visualisation/make_window_convergence.jl new file mode 100644 index 0000000..6b3f256 --- /dev/null +++ b/visualisation/make_window_convergence.jl @@ -0,0 +1,840 @@ +#!/usr/bin/env julia + +using Pkg +Pkg.activate(joinpath(@__DIR__, "..")) + +using CairoMakie +using CUDA +using Dates +using JLD2 +using JSON3 +using Printf +using Random +using Statistics +using TranscranialFUS + +const PROJECT_ROOT = normpath(joinpath(@__DIR__, "..")) + +parse_bool(s) = lowercase(strip(String(s))) in ("1", "true", "yes", "on") + +function parse_cli(args) + opts = Dict{String, String}( + "out-dir" => "", + "random-seed" => "42", + "aberrator" => "skull", + "t-max-us" => "300", + "dy-mm" => "0.2", + "dz-mm" => "0.2", + "max-windows" => "0", + "fps" => "2", + "frames-only" => "false", + "dry-run" => "false", + "recon-progress" => "false", + "kwave-use-gpu" => "true", + "recon-use-gpu" => "true", + "zero-pad-factor" => "2", + "from-data" => "", + ) + for arg in args + startswith(arg, "--") || error("Unsupported argument format: $arg") + parts = split(arg[3:end], "="; limit=2) + length(parts) == 2 || error("Arguments must use --name=value, got: $arg") + opts[parts[1]] = parts[2] + end + return opts +end + +function timestamp() + return Dates.format(Dates.now(), "yyyymmdd_HHMMSS") +end + +function parse_aberrator(value::AbstractString) + sym = Symbol(lowercase(strip(value))) + sym in (:none, :water, :skull) || error("--aberrator must be none, water, or skull.") + return sym == :water ? :none : sym +end + +function make_demo_config(opts) + dy = parse(Float64, opts["dy-mm"]) * 1e-3 + dz = parse(Float64, opts["dz-mm"]) * 1e-3 + return PAMConfig3D( + dx = 0.2e-3, + dy = dy, + dz = dz, + axial_dim = 70e-3, + transverse_dim_y = 64e-3, + transverse_dim_z = 64e-3, + dt = 40e-9, + t_max = parse(Float64, opts["t-max-us"]) * 1e-6, + receiver_aperture_y = nothing, + receiver_aperture_z = nothing, + zero_pad_factor = parse(Int, opts["zero-pad-factor"]), + peak_suppression_radius = 2e-3, + success_tolerance = 1e-3, + axial_gain_power = 1.5, + ) +end + +function make_demo_sources(cfg::PAMConfig3D, seed::Integer) + rng = Random.MersenneTwister(seed) + centers = [(45e-3, 0.0, 0.0)] + sources, meta = make_network_bubble_sources_3d( + centers; + axial_radius = 10e-3, + lateral_y_radius = 1.5e-3, + lateral_z_radius = 1.5e-3, + root_count = 12, + generations = 3, + branch_length = 5.0e-3, + branch_step = 0.4e-3, + branch_angle = 36 * pi / 180, + tortuosity = 0.18, + network_orientation = :isotropic, + source_spacing = 0.5e-3, + density_sigma_depth = 10.0e-3, + density_sigma_y = 1.5e-3, + density_sigma_z = 1.5e-3, + min_separation = 0.25e-3, + max_sources_per_center = 80, + depth_bounds = (0.0, cfg.axial_dim), + lateral_y_bounds = (-cfg.transverse_dim_y / 2, cfg.transverse_dim_y / 2), + lateral_z_bounds = (-cfg.transverse_dim_z / 2, cfg.transverse_dim_z / 2), + fundamental = 0.5e6, + harmonics = [2, 3, 4], + harmonic_amplitudes = [1.0, 0.6, 0.3], + gate_duration = 50e-6, + taper_ratio = 0.25, + # The actual simulated events are expanded below with fresh random + # phases per reconstruction window. Keep the template sources neutral + # so the saved metadata does not imply geometric phase driving. + phase_mode = :random, + transducer_depth = -30e-3, + rng = rng, + ) + meta[:activity_model] = Dict( + "activity_mode" => "random_phase_per_window", + "template_phase_mode" => "random", + ) + return sources, meta +end + +function default_recon_frequencies(sources) + freqs = Float64[] + for src in sources + append!(freqs, emission_frequencies(src)) + end + return sort(unique(freqs)) +end + +function source_detected_flags_3d(pred, grid, cfg::PAMConfig3D, sources; radius::Real) + radius_m = Float64(radius) + x = collect(grid.x) + y = collect(grid.y) + z = collect(grid.z) + r0 = receiver_row(cfg) + row_r = ceil(Int, radius_m / cfg.dx) + col_r_y = ceil(Int, radius_m / cfg.dy) + col_r_z = ceil(Int, radius_m / cfg.dz) + radius2 = radius_m^2 + + flags = falses(length(sources)) + distances_mm = Float64[] + for (idx, src) in pairs(sources) + src_x = x[r0] + src.depth + row0 = r0 + round(Int, src.depth / cfg.dx) + col0_y = argmin(abs.(y .- src.lateral_y)) + col0_z = argmin(abs.(z .- src.lateral_z)) + best_d2 = Inf + for row in max(r0 + 1, row0 - row_r):min(size(pred, 1), row0 + row_r) + dx2 = (x[row] - src_x)^2 + for iy in max(1, col0_y - col_r_y):min(size(pred, 2), col0_y + col_r_y) + dy2 = (y[iy] - src.lateral_y)^2 + for iz in max(1, col0_z - col_r_z):min(size(pred, 3), col0_z + col_r_z) + pred[row, iy, iz] || continue + d2 = dx2 + dy2 + (z[iz] - src.lateral_z)^2 + if d2 <= radius2 && d2 < best_d2 + best_d2 = d2 + end + end + end + end + if isfinite(best_d2) + flags[idx] = true + push!(distances_mm, sqrt(best_d2) * 1e3) + end + end + return flags, distances_mm +end + +function fixed_threshold_stats_3d(intensity, cutoff::Real, truth_mask, grid, cfg, sources; truth_radius::Real) + pred = intensity .>= Float64(cutoff) + tp = count(pred .& truth_mask) + fp = count(pred .& .!truth_mask) + fn = count(.!pred .& truth_mask) + precision = tp + fp == 0 ? 0.0 : tp / (tp + fp) + voxel_recall = tp + fn == 0 ? 0.0 : tp / (tp + fn) + voxel_f1 = precision + voxel_recall == 0 ? 0.0 : 2 * precision * voxel_recall / (precision + voxel_recall) + flags, distances_mm = source_detected_flags_3d(pred, grid, cfg, sources; radius=truth_radius) + detected = count(identity, flags) + source_recall = isempty(sources) ? 0.0 : detected / length(sources) + source_f1 = precision + source_recall == 0 ? 0.0 : 2 * precision * source_recall / (precision + source_recall) + return Dict{Symbol, Any}( + :source_f1 => source_f1, + :voxel_f1 => voxel_f1, + :precision => precision, + :source_recall => source_recall, + :voxel_recall => voxel_recall, + :detected_source_count => detected, + :num_truth_sources => length(sources), + :predicted_voxels => count(pred), + :true_positive_voxels => tp, + :false_positive_voxels => fp, + :false_negative_voxels => fn, + :truth_voxels => count(truth_mask), + :mean_detected_source_distance_mm => isempty(distances_mm) ? nothing : mean(distances_mm), + :max_detected_source_distance_mm => isempty(distances_mm) ? nothing : maximum(distances_mm), + :detected_flags => flags, + ) +end + +function best_final_threshold(final_intensity, truth_mask, grid, cfg, sources; truth_radius::Real) + local_ref = max(maximum(Float64.(final_intensity)), eps(Float64)) + thresholds = collect(0.10:0.01:0.95) + entries = threshold_detection_stats_3d( + final_intensity, + grid, + cfg, + sources; + threshold_ratios=thresholds, + truth_radius=truth_radius, + truth_mask=truth_mask, + ) + best = first(entries) + for entry in entries[2:end] + score = (Float64(entry[:voxel_f1]), Float64(entry[:precision]), Float64(entry[:threshold_ratio])) + best_score = (Float64(best[:voxel_f1]), Float64(best[:precision]), Float64(best[:threshold_ratio])) + if score > best_score + best = entry + end + end + best[:absolute_cutoff] = Float64(best[:threshold_ratio]) * local_ref + return best, entries +end + +function qualify_windows(rf, cfg, window_config, max_windows::Integer) + nt = size(rf, 3) + ranges, window_samples, hop_samples = TranscranialFUS._pam_window_ranges(nt, cfg.dt, window_config) + energies = [sum(abs2, @view rf[:, :, range]) for range in ranges] + max_energy = isempty(energies) ? 0.0 : maximum(energies) + threshold = max_energy * window_config.min_energy_ratio + selected = UnitRange{Int}[] + selected_energy = Float64[] + skipped = UnitRange{Int}[] + for (range, energy) in zip(ranges, energies) + if energy < threshold || energy <= 0 + push!(skipped, range) + else + push!(selected, range) + push!(selected_energy, Float64(energy)) + end + end + if max_windows > 0 && length(selected) > max_windows + selected = selected[1:max_windows] + selected_energy = selected_energy[1:max_windows] + end + return selected, selected_energy, skipped, window_samples, hop_samples, threshold +end + +function reconstruct_or_load_window(recons_dir, idx, rf, c, cfg, range, window_config, recon_frequencies, bandwidth_hz; + use_gpu::Bool, show_progress::Bool) + path = joinpath(recons_dir, @sprintf("window_%04d.jld2", idx)) + if isfile(path) + d = load(path) + return d["intensity"], d["grid"] + end + intensity, grid = reconstruct_one_window(rf, c, cfg, range, window_config, recon_frequencies, bandwidth_hz; + use_gpu=use_gpu, show_progress=show_progress) + @save path intensity grid + return intensity, grid +end + +function reconstruct_one_window(rf, c, cfg, range, window_config, recon_frequencies, bandwidth_hz; + use_gpu::Bool, show_progress::Bool) + taper = TranscranialFUS._pam_temporal_taper(length(range), window_config.taper) + rf_window = Float64.(@view rf[:, :, range]) .* reshape(taper, 1, 1, :) + t0 = Float64(first(range) - 1) * cfg.dt + intensity, grid, _ = reconstruct_pam_3d( + rf_window, + c, + cfg; + frequencies = recon_frequencies, + bandwidth = bandwidth_hz, + corrected = true, + axial_step = 50e-6, + time_origin = t0, + use_gpu = use_gpu, + show_progress = show_progress, + window_batch = 1, + ) + return intensity, grid +end + + +function threshold_voxel_points(mask, intensity, grid, cfg; max_points::Int=25000) + idxs = findall(mask) + isempty(idxs) && return Float64[], Float64[], Float64[], Float64[] + if length(idxs) > max_points + keep = unique(round.(Int, range(1, length(idxs); length=max_points))) + idxs = idxs[keep] + end + rr = receiver_row(cfg) + xs = Float64[] + ys = Float64[] + zs = Float64[] + cs = Float64[] + max_i = max(maximum(Float64.(intensity)), eps(Float64)) + sizehint!(xs, length(idxs)) + sizehint!(ys, length(idxs)) + sizehint!(zs, length(idxs)) + sizehint!(cs, length(idxs)) + for idx in idxs + push!(xs, grid.y[idx[2]] * 1e3) + push!(ys, grid.z[idx[3]] * 1e3) + push!(zs, (grid.x[idx[1]] - grid.x[rr]) * 1e3) + push!(cs, Float64(intensity[idx]) / max_i) + end + return xs, ys, zs, cs +end + +function precompute_skull_surface(c, grid, cfg, medium_info; c_threshold=1600.0, stride=2) + rr = receiver_row(cfg) + nx, ny, nz = size(c) + row_start = haskey(medium_info, :outer_row) ? max(1, Int(medium_info[:outer_row]) - 3) : 1 + row_end = haskey(medium_info, :inner_row) ? min(nx, Int(medium_info[:inner_row]) + 3) : nx + + iy_range = 1:stride:ny + iz_range = 1:stride:nz + n_y = length(iy_range) + n_z = length(iz_range) + + ys = Float32[grid.y[iy] * 1e3 for iy in iy_range] + zs = Float32[grid.z[iz] * 1e3 for iz in iz_range] + outer_d = fill(NaN32, n_y, n_z) + outer_sos = fill(NaN32, n_y, n_z) + + for (jy, iy) in enumerate(iy_range) + for (jz, iz) in enumerate(iz_range) + found = false + for ix in row_start:row_end + if Float64(c[ix, iy, iz]) > c_threshold + outer_d[jy, jz] = Float32((grid.x[ix] - grid.x[rr]) * 1e3) + outer_sos[jy, jz] = Float32(c[ix, iy, iz]) + found = true + break + end + end + end + end + + has_skull = !all(isnan, outer_d) + return (; ys, zs, outer_d, outer_sos, has_skull) +end + +function compute_axis_limits(grid, cfg) + rr = receiver_row(cfg) + return ( + y_min = minimum(grid.y) * 1e3, + y_max = maximum(grid.y) * 1e3, + z_min = minimum(grid.z) * 1e3, + z_max = maximum(grid.z) * 1e3, + d_min = (minimum(grid.x) - grid.x[rr]) * 1e3, + d_max = (maximum(grid.x) - grid.x[rr]) * 1e3, + ) +end + +function render_frame(path, cumulative, current, fixed_cutoff, best_ratio, grid, cfg, sources, centerlines, skull_surf, ax_limits, metrics_history, stats, frame_idx, total_frames) + pred_mask = cumulative .>= fixed_cutoff + current_mask = current .>= fixed_cutoff + flags = stats[:detected_flags] + + update_theme!(fontsize=14) + fig = Figure(size=(1700, 960), backgroundcolor=:black) + + # ── 3D reconstruction view ──────────────────────────────────────────────── + ax3 = Axis3( + fig[1, 1]; + xlabel="Y [mm]", + ylabel="Z [mm]", + zlabel="Depth [mm]", + title="3D cumulative HASA — window $frame_idx / $total_frames", + perspectiveness=0.35, + azimuth=5pi / 8, + elevation=pi / 7, + backgroundcolor=:black, + titlecolor=:white, + xlabelcolor=:white, + ylabelcolor=:white, + zlabelcolor=:white, + xticklabelcolor=:white, + yticklabelcolor=:white, + zticklabelcolor=:white, + ) + xlims!(ax3, ax_limits.y_min, ax_limits.y_max) + ylims!(ax3, ax_limits.z_min, ax_limits.z_max) + zlims!(ax3, ax_limits.d_min, ax_limits.d_max) + + # ── Receiver array plane at depth 0 ────────────────────────────────────── + let ry = [ax_limits.y_min, ax_limits.y_max], + rz = [ax_limits.z_min, ax_limits.z_max] + surface!(ax3, ry, rz, zeros(Float32, 2, 2); color=(:orchid, 0.18), shading=NoShading) + yc = [ax_limits.y_min, ax_limits.y_max, ax_limits.y_max, ax_limits.y_min, ax_limits.y_min] + zc = [ax_limits.z_min, ax_limits.z_min, ax_limits.z_max, ax_limits.z_max, ax_limits.z_min] + lines!(ax3, yc, zc, zeros(5); color=(:orchid, 0.8), linewidth=2.0) + end + + # ── Skull outer surface ─────────────────────────────────────────────────── + if skull_surf.has_skull + sos_norm = clamp.((skull_surf.outer_sos .- 1600.0f0) ./ (2500.0f0 - 1600.0f0), 0.0f0, 1.0f0) + surface!(ax3, skull_surf.ys, skull_surf.zs, skull_surf.outer_d; + color=sos_norm, colormap=:Greys, colorrange=(0.0, 1.0), + alpha=0.65, shading=NoShading) + end + + for line in centerlines + ys = [p[2] * 1e3 for p in line] + zs = [p[3] * 1e3 for p in line] + ds = [p[1] * 1e3 for p in line] + lines!(ax3, ys, zs, ds; color=(:white, 0.55), linewidth=1.1) + end + + vx, vy, vz, vc = threshold_voxel_points(pred_mask, cumulative, grid, cfg; max_points=22000) + if !isempty(vx) + scatter!(ax3, vx, vy, vz; color=vc, colormap=:Oranges, colorrange=(0.0, 1.0), markersize=2.2, alpha=0.4) + end + + cx, cy, cz, _ = threshold_voxel_points(current_mask, current, grid, cfg; max_points=10000) + if !isempty(cx) + scatter!(ax3, cx, cy, cz; color=(:deepskyblue, 0.3), markersize=2.0) + end + + src_y = [src.lateral_y * 1e3 for src in sources] + src_z = [src.lateral_z * 1e3 for src in sources] + src_d = [src.depth * 1e3 for src in sources] + src_colors = [flags[i] ? :lime : (:gray80, 0.45) for i in eachindex(sources)] + scatter!(ax3, src_y, src_z, src_d; color=src_colors, markersize=8, strokecolor=:black, strokewidth=0.7) + + # ── Metrics convergence plot ────────────────────────────────────────────── + ax_m = Axis( + fig[1, 2]; + xlabel="window", + title="convergence", + backgroundcolor=:black, + titlecolor=:white, + xlabelcolor=:white, + ylabelcolor=:white, + xticklabelcolor=:white, + yticklabelcolor=:white, + xgridcolor=(:white, 0.12), + ygridcolor=(:white, 0.12), + bottomspinecolor=:white, + leftspinecolor=:white, + topspinecolor=:transparent, + rightspinecolor=:transparent, + yticks=0.0:0.2:1.0, + ) + xlims!(ax_m, 0.5, total_frames + 0.5) + ylims!(ax_m, 0.0, 1.0) + + ws = Float64[m[:frame_index] for m in metrics_history] + pr = Float64[m[:precision] for m in metrics_history] + rc = Float64[m[:voxel_recall] for m in metrics_history] + f1 = Float64[m[:voxel_f1] for m in metrics_history] + + lines!(ax_m, ws, pr; color=:deepskyblue, linewidth=2, label="precision") + lines!(ax_m, ws, rc; color=:lime, linewidth=2, label="recall") + lines!(ax_m, ws, f1; color=:orange, linewidth=2, label="voxel F1") + scatter!(ax_m, ws, pr; color=:deepskyblue, markersize=7) + scatter!(ax_m, ws, rc; color=:lime, markersize=7) + scatter!(ax_m, ws, f1; color=:orange, markersize=7) + vlines!(ax_m, [Float64(frame_idx)]; color=(:white, 0.35), linewidth=1.2, linestyle=:dash) + axislegend(ax_m; position=:lb, backgroundcolor=(:black, 0.8), labelcolor=:white, + framevisible=false, labelsize=13, padding=(4, 4, 4, 4)) + + text!(ax_m, 0.97, 0.97; + text=@sprintf("prec: %.3f\nrecall: %.3f\nF1: %.3f", + Float64(stats[:precision]), + Float64(stats[:voxel_recall]), + Float64(stats[:voxel_f1])), + color=:white, fontsize=13, align=(:right, :top), space=:relative, + ) + + # ── Stats bar ──────────────────────────────────────────────────────────── + stats_text = @sprintf( + "threshold: %.2f of final max (voxel F1-optimal) voxel F1: %.3f precision: %.3f voxel recall: %.3f TP: %d FP: %d FN: %d predicted: %d / truth: %d voxels", + best_ratio, + Float64(stats[:voxel_f1]), + Float64(stats[:precision]), + Float64(stats[:voxel_recall]), + Int(stats[:true_positive_voxels]), + Int(stats[:false_positive_voxels]), + Int(stats[:false_negative_voxels]), + Int(stats[:predicted_voxels]), + Int(stats[:truth_voxels]), + ) + Label(fig[2, 1:2], stats_text; color=:white, fontsize=14, halign=:center, tellheight=true) + + Legend( + fig[3, 1:2], + [ + LineElement(color=(:orchid, 0.9), linewidth=2), + PolyElement(color=(:gray60, 0.6)), + LineElement(color=(:white, 0.7), linewidth=2), + MarkerElement(color=:orange, marker=:circle, markersize=10), + MarkerElement(color=(:deepskyblue, 0.6), marker=:circle, markersize=10), + MarkerElement(color=:lime, marker=:circle, markersize=10), + MarkerElement(color=(:gray80, 0.45), marker=:circle, markersize=10), + ], + ["receiver array", "skull surface", "vascular network", "cumulative HASA", "current window", "detected source", "not yet detected"]; + orientation=:horizontal, + framevisible=false, + labelcolor=:white, + tellheight=true, + ) + + # ── Column / row sizing (must come after all content is placed) ─────────── + colsize!(fig.layout, 1, Relative(0.62)) + rowsize!(fig.layout, 2, Fixed(38)) + rowsize!(fig.layout, 3, Fixed(48)) + + save(path, fig) + return path +end + +function find_ffmpeg() + ffmpeg = Sys.which("ffmpeg") + !isnothing(ffmpeg) && return ffmpeg + if Sys.iswindows() && haskey(ENV, "LOCALAPPDATA") + winget_root = joinpath(ENV["LOCALAPPDATA"], "Microsoft", "WinGet", "Packages") + if isdir(winget_root) + for (root, _, files) in walkdir(winget_root) + if "ffmpeg.exe" in files + return joinpath(root, "ffmpeg.exe") + end + end + end + end + return nothing +end + +function encode_mp4(frames_dir, mp4_path, fps::Integer) + ffmpeg = find_ffmpeg() + pattern = joinpath(frames_dir, "frame_%04d.png") + if isnothing(ffmpeg) + println("ffmpeg not found on PATH. Frames are ready in: $frames_dir") + println("Encode with:") + println("ffmpeg -y -framerate $fps -i \"$pattern\" -vf format=yuv420p \"$mp4_path\"") + return false + end + cmd = Cmd([ffmpeg, "-y", "-framerate", string(fps), "-i", pattern, "-vf", "format=yuv420p", mp4_path]) + run(cmd) + return true +end + +function print_dry_run(opts, cfg, fitted_cfg, sources, out_dir, window_config) + nt = pam_Nt(fitted_cfg) + ranges, window_samples, hop_samples = TranscranialFUS._pam_window_ranges(nt, fitted_cfg.dt, window_config) + println("Dry run: PAM window convergence visualisation") + println(" output dir : $out_dir") + println(" aberrator : ", opts["aberrator"]) + println(" sources : ", length(sources)) + println(" grid : $(pam_Nx(fitted_cfg)) x $(pam_Ny(fitted_cfg)) x $(pam_Nz(fitted_cfg))") + println(" spacing mm : dx=$(fitted_cfg.dx * 1e3), dy=$(fitted_cfg.dy * 1e3), dz=$(fitted_cfg.dz * 1e3)") + println(" t_max us : ", fitted_cfg.t_max * 1e6) + println(" nt : $nt") + println(" windows : $(length(ranges))") + println(" window samples : $window_samples") + println(" hop samples : $hop_samples") + println(" recon freqs MHz : ", join(round.(default_recon_frequencies(sources) ./ 1e6; digits=3), ", ")) + println(" CUDA functional : ", CUDA.functional()) + println(" k-Wave available : ", kwave_available()) + println(" ffmpeg : ", something(find_ffmpeg(), "not found")) + return nothing +end + +function load_cached_inputs(path::AbstractString) + d = load(path) + required = ["cfg", "sources", "network_meta", "c", "rho", "medium_info", "rf", "grid", "simulation_info"] + missing = filter(key -> !haskey(d, key), required) + isempty(missing) || error("--from-data is missing required keys: $(join(missing, ", "))") + return ( + cfg = d["cfg"], + sources = d["sources"], + network_meta = d["network_meta"], + c = d["c"], + rho = d["rho"], + medium_info = d["medium_info"], + rf = d["rf"], + grid = d["grid"], + simulation_info = d["simulation_info"], + sim_sources = haskey(d, "sim_sources") ? d["sim_sources"] : EmissionSource3D[], + n_frames = haskey(d, "n_frames") ? d["n_frames"] : missing, + ) +end + +function main() + opts = parse_cli(ARGS) + from_data = strip(opts["from-data"]) + seed = parse(Int, opts["random-seed"]) + aberrator = parse_aberrator(opts["aberrator"]) + dry_run = parse_bool(opts["dry-run"]) + frames_only = parse_bool(opts["frames-only"]) + fps = parse(Int, opts["fps"]) + max_windows = parse(Int, opts["max-windows"]) + use_gpu_sim = parse_bool(opts["kwave-use-gpu"]) + use_gpu_recon = parse_bool(opts["recon-use-gpu"]) + show_progress = parse_bool(opts["recon-progress"]) + + out_dir = if !isempty(strip(opts["out-dir"])) + abspath(opts["out-dir"]) + elseif !isempty(from_data) + dirname(abspath(from_data)) + else + joinpath(@__DIR__, "outputs", "$(timestamp())_window_convergence") + end + frames_dir = joinpath(out_dir, "frames") + recons_dir = joinpath(out_dir, "recons") + data_path = joinpath(out_dir, "data.jld2") + summary_path = joinpath(out_dir, "summary.json") + mp4_path = joinpath(out_dir, "pam_window_convergence.mp4") + + window_config = PAMWindowConfig( + enabled=true, + window_duration=40e-6, + hop=20e-6, + taper=:hann, + min_energy_ratio=0.001, + accumulation=:intensity, + ) + + cfg_base = make_demo_config(opts) + if isempty(from_data) + sources, network_meta = make_demo_sources(cfg_base, seed) + cfg = fit_pam_config_3d( + cfg_base, + sources; + min_bottom_margin=10e-3, + reference_depth=aberrator == :skull ? 20e-3 : nothing, + ) + else + cached = load_cached_inputs(abspath(from_data)) + cfg = cached.cfg + sources = cached.sources + network_meta = cached.network_meta + end + + if dry_run + print_dry_run(opts, cfg_base, cfg, sources, out_dir, window_config) + if !isempty(from_data) + println(" from data : ", abspath(from_data)) + end + return nothing + end + + mkpath(frames_dir) + mkpath(recons_dir) + + if isempty(from_data) + println("Building medium...") + c, rho, medium_info = make_pam_medium_3d( + cfg; + aberrator=aberrator, + skull_to_transducer=20e-3, + slice_index_z=250, + ) + + println("Expanding random-phase-per-window source events...") + rng_sim = Random.MersenneTwister(seed + 1) + sim_sources, n_frames = TranscranialFUS._expand_sources_per_window( + sources, + window_config.window_duration, + window_config.hop, + cfg.t_max, + rng_sim; + variability=SourceVariabilityConfig(frequency_jitter_fraction=0.01), + ) + + println("Simulating RF data with $(length(sim_sources)) emission events...") + rf, grid, simulation_info = simulate_point_sources_3d(c, rho, sim_sources, cfg; use_gpu=use_gpu_sim) + println("Saving RF/medium cache to $data_path") + @save data_path cfg sources network_meta c rho medium_info rf grid simulation_info sim_sources n_frames window_config + else + println("Loading cached RF/medium data from $(abspath(from_data))") + cached = load_cached_inputs(abspath(from_data)) + c = cached.c + rho = cached.rho + medium_info = cached.medium_info + rf = cached.rf + grid = cached.grid + simulation_info = cached.simulation_info + sim_sources = cached.sim_sources + n_frames = cached.n_frames + end + + selected_ranges, selected_energy, skipped_ranges, window_samples, hop_samples, energy_threshold = + qualify_windows(rf, cfg, window_config, max_windows) + isempty(selected_ranges) && error("No qualifying windows were found.") + + recon_frequencies = default_recon_frequencies(sources) + bandwidth_hz = 40e3 + truth_radius = 1.0e-3 + truth_mask = pam_truth_mask_3d(sources, grid, cfg; radius=truth_radius) + centerlines = network_meta[:centerlines] + + println("Precomputing skull scatter points...") + skull_surf = precompute_skull_surface(c, grid, cfg, medium_info) + println(" skull surface: $(skull_surf.has_skull ? "$(sum(.!isnan.(skull_surf.outer_d))) bone pixels" : "none (water run)")") + + println("Accumulating $(length(selected_ranges)) reconstructed windows to choose final threshold...") + cumulative_sum = zeros(Float64, pam_Nx(cfg), pam_Ny(cfg), pam_Nz(cfg)) + final_grid = grid + for (idx, range) in pairs(selected_ranges) + @printf(" accumulation window %d/%d samples %d:%d\n", idx, length(selected_ranges), first(range), last(range)) + current, final_grid = reconstruct_or_load_window( + recons_dir, idx, rf, c, cfg, range, window_config, recon_frequencies, bandwidth_hz; + use_gpu=use_gpu_recon, + show_progress=show_progress, + ) + cumulative_sum .+= current + GC.gc(false) + CUDA.reclaim() + end + final_cumulative = cumulative_sum ./ length(selected_ranges) + best_threshold, threshold_entries = best_final_threshold( + final_cumulative, + truth_mask, + final_grid, + cfg, + sources; + truth_radius=truth_radius, + ) + fixed_cutoff = Float64(best_threshold[:absolute_cutoff]) + best_ratio = Float64(best_threshold[:threshold_ratio]) + ax_limits = compute_axis_limits(grid, cfg) + @printf("Selected final threshold %.2f of final max: voxel F1 %.3f, precision %.3f, voxel recall %.3f\n", + best_ratio, + Float64(best_threshold[:voxel_f1]), + Float64(best_threshold[:precision]), + Float64(best_threshold[:voxel_recall]), + ) + + metrics_by_frame = Dict{Symbol, Any}[] + println("Rendering frames with fixed final threshold...") + fill!(cumulative_sum, 0.0) + for (idx, range) in pairs(selected_ranges) + @printf(" render window %d/%d\n", idx, length(selected_ranges)) + current, final_grid = reconstruct_or_load_window( + recons_dir, idx, rf, c, cfg, range, window_config, recon_frequencies, bandwidth_hz; + use_gpu=use_gpu_recon, + show_progress=show_progress, + ) + cumulative_sum .+= current + cumulative = cumulative_sum ./ idx + stats = fixed_threshold_stats_3d(cumulative, fixed_cutoff, truth_mask, final_grid, cfg, sources; truth_radius=truth_radius) + stats[:frame_index] = idx + stats[:sample_range] = [first(range), last(range)] + push!(metrics_by_frame, copy(stats)) + frame_path = joinpath(frames_dir, @sprintf("frame_%04d.png", idx)) + render_frame( + frame_path, + cumulative, + current, + fixed_cutoff, + best_ratio, + final_grid, + cfg, + sources, + centerlines, + skull_surf, + ax_limits, + metrics_by_frame, + stats, + idx, + length(selected_ranges), + ) + GC.gc(false) + CUDA.reclaim() + end + + summary = Dict( + "out_dir" => out_dir, + "data_path" => data_path, + "frames_dir" => frames_dir, + "mp4_path" => mp4_path, + "random_seed" => seed, + "aberrator" => String(aberrator), + "from_data" => isempty(from_data) ? nothing : abspath(from_data), + "grid" => Dict( + "nx" => pam_Nx(cfg), + "ny" => pam_Ny(cfg), + "nz" => pam_Nz(cfg), + "nt" => pam_Nt(cfg), + "dx_m" => cfg.dx, + "dy_m" => cfg.dy, + "dz_m" => cfg.dz, + "t_max_s" => cfg.t_max, + "zero_pad_factor" => cfg.zero_pad_factor, + ), + "source_model" => "network3d", + "source_phase_mode" => "random_phase_per_window", + "source_count" => length(sources), + "emission_event_count" => length(sim_sources), + "n_frames_from_source_expansion" => n_frames, + "windowing" => Dict( + "window_duration_s" => window_config.window_duration, + "hop_s" => window_config.hop, + "window_samples" => window_samples, + "hop_samples" => hop_samples, + "selected_window_count" => length(selected_ranges), + "skipped_window_count" => length(skipped_ranges), + "energy_threshold" => energy_threshold, + ), + "reconstruction" => Dict( + "frequencies_hz" => recon_frequencies, + "bandwidth_hz" => bandwidth_hz, + "axial_step_m" => 50e-6, + "use_gpu" => use_gpu_recon, + ), + "threshold" => Dict( + "mode" => "final_best_source_f1_fixed_absolute_cutoff", + "ratio_of_final_max" => best_ratio, + "absolute_cutoff" => fixed_cutoff, + "best_final_metrics" => best_threshold, + ), + "medium" => Dict(String(k) => v for (k, v) in medium_info), + "simulation" => Dict(String(k) => v for (k, v) in simulation_info), + "sources" => [source_summary(src) for src in sources], + "metrics_by_frame" => metrics_by_frame, + ) + + println("Saving data bundle...") + selected_ranges_pairs = [[first(r), last(r)] for r in selected_ranges] + skipped_ranges_pairs = [[first(r), last(r)] for r in skipped_ranges] + @save data_path cfg sources network_meta c rho medium_info rf grid simulation_info sim_sources n_frames window_config recon_frequencies bandwidth_hz truth_radius truth_mask final_cumulative best_threshold threshold_entries selected_ranges_pairs skipped_ranges_pairs metrics_by_frame + + open(summary_path, "w") do io + JSON3.pretty(io, summary) + end + + if frames_only + println("Frames written to $frames_dir") + else + encoded = encode_mp4(frames_dir, mp4_path, fps) + encoded && println("Saved MP4 to $mp4_path") + end + println("Done: $out_dir") + return nothing +end + +if abspath(PROGRAM_FILE) == @__FILE__ + main() +end diff --git a/visualisation/napari/README.md b/visualisation/napari/README.md new file mode 100644 index 0000000..49d4732 --- /dev/null +++ b/visualisation/napari/README.md @@ -0,0 +1,52 @@ +# napari PAM convergence viewer + +## Dependencies + +``` +pip install napari[all] h5py imageio imageio-ffmpeg +``` + +## Usage + +### 1. Export (once per run) + +```powershell +julia --project=. visualisation/napari/export_for_napari.jl ` + --run-dir=visualisation/outputs/20260509_170538_window_convergence/rerender_2 +``` + +Reads `data.jld2` and `recons/window_XXXX.jld2` from the run folder, +writes `napari_data.h5`. + +### 2. Interactive viewer (slider to scrub) + +```powershell +python visualisation/napari/show_convergence.py ` + --run-dir=visualisation/outputs/20260509_170538_window_convergence/rerender_2 +``` + +Opens napari in 3D mode. The cumulative HASA reconstruction is a 4-D layer +`(window × depth × y × z)` — use the slider to scrub through convergence. + +### 3. Cinematic animation + +```powershell +python visualisation/napari/show_convergence.py ` + --run-dir=visualisation/outputs/20260509_170538_window_convergence/rerender_2 ` + --animate --fps 24 --orbit-frames 72 +``` + +Saves `napari_cinematic.mp4` in the run folder. + +Two phases: +- **Phase 1** — skull orbit: camera rotates 360° over `--orbit-frames` frames +- **Phase 2** — convergence build-up: camera locks, steps through each window + +## Options + +| Flag | Default | Description | +|---|---|---| +| `--run-dir` | *(required)* | Path to the window convergence run folder | +| `--animate` | off | Generate MP4 instead of opening interactive viewer | +| `--fps` | 24 | Animation frame rate | +| `--orbit-frames` | 72 | Frames for the skull-orbit intro (72 = 3 s at 24 fps) | diff --git a/visualisation/napari/export_for_napari.jl b/visualisation/napari/export_for_napari.jl new file mode 100644 index 0000000..f7a7cb4 --- /dev/null +++ b/visualisation/napari/export_for_napari.jl @@ -0,0 +1,103 @@ +#!/usr/bin/env julia +# Exports a window-convergence run to a single HDF5 file readable by show_convergence.py. +# +# Usage: +# julia --project=. visualisation/napari/export_for_napari.jl --run-dir=visualisation/outputs/_window_convergence +# +# Reads: /data.jld2 and /recons/window_XXXX.jld2 +# Writes: /napari_data.h5 + +using Pkg +Pkg.activate(joinpath(@__DIR__, "../..")) + +using HDF5 +using JLD2 +using Printf +using TranscranialFUS + +function parse_run_dir(args) + for arg in args + m = match(r"^--run-dir=(.+)$", arg) + isnothing(m) || return strip(m.captures[1]) + end + error("Required argument --run-dir= not found.") +end + +function main() + run_dir = abspath(parse_run_dir(ARGS)) + data_path = joinpath(run_dir, "data.jld2") + recons_dir = joinpath(run_dir, "recons") + out_path = joinpath(run_dir, "napari_data.h5") + + isfile(data_path) || error("data.jld2 not found in $run_dir") + + println("Loading data from $data_path ...") + d = load(data_path) + + cfg = d["cfg"] + sources = d["sources"] + network_meta = d["network_meta"] + grid = d["grid"] + c = d["c"] # Float32 (nx, ny, nz) — SOS field + + rr = receiver_row(cfg) + x_mm = Float32.(collect(grid.x) .* 1e3) + y_mm = Float32.(collect(grid.y) .* 1e3) + z_mm = Float32.(collect(grid.z) .* 1e3) + + println("Computing truth mask ...") + truth_mask = pam_truth_mask_3d(sources, grid, cfg; radius=1.0e-3) + + src_depth_mm = Float32[src.depth * 1e3 for src in sources] + src_y_mm = Float32[src.lateral_y * 1e3 for src in sources] + src_z_mm = Float32[src.lateral_z * 1e3 for src in sources] + + # Flatten centerlines to (N,3) and a parallel track-id vector + centerlines = network_meta[:centerlines] + cl_depth = Float32[] + cl_y = Float32[] + cl_z = Float32[] + cl_ids = Int32[] + for (tid, line) in enumerate(centerlines) + for pt in line + push!(cl_depth, Float32(pt[1] * 1e3)) + push!(cl_y, Float32(pt[2] * 1e3)) + push!(cl_z, Float32(pt[3] * 1e3)) + push!(cl_ids, Int32(tid)) + end + end + + # Per-window intensities + window_files = sort(filter(f -> startswith(f, "window_") && endswith(f, ".jld2"), + readdir(recons_dir))) + isempty(window_files) && println("Warning: no window_XXXX.jld2 found in $recons_dir") + + println("Writing $out_path ...") + h5open(out_path, "w") do f + f["sos"] = Float32.(c) + f["x_mm"] = x_mm + f["y_mm"] = y_mm + f["z_mm"] = z_mm + f["receiver_row"] = Int32(rr) + f["source_depth_mm"] = src_depth_mm + f["source_y_mm"] = src_y_mm + f["source_z_mm"] = src_z_mm + f["truth_mask"] = UInt8.(truth_mask) + if !isempty(cl_ids) + f["centerline_depth_mm"] = cl_depth + f["centerline_y_mm"] = cl_y + f["centerline_z_mm"] = cl_z + f["centerline_ids"] = cl_ids + end + for wf in window_files + @printf(" loading %s ...\n", wf) + wd = load(joinpath(recons_dir, wf)) + key = splitext(wf)[1] + f[key] = Float32.(wd["intensity"]) + end + end + + println("Done: $out_path") +end + +main() diff --git a/visualisation/napari/show_convergence.py b/visualisation/napari/show_convergence.py new file mode 100644 index 0000000..50344aa --- /dev/null +++ b/visualisation/napari/show_convergence.py @@ -0,0 +1,303 @@ +""" +Napari PAM window convergence viewer. + +Loads a napari_data.h5 export and displays the SOS field, vascular network, +truth sources, and cumulative HASA reconstruction. + +Interactive mode (default): + The cumulative HASA stack is a 4-D layer (window × depth × y × z) with a + slider so you can scrub through convergence manually. + +Animation mode (--animate): + Two-phase cinematic MP4: + Phase 1 — camera orbits 360° around the skull + vascular network + Phase 2 — camera locks at a fixed view; cumulative HASA builds up + window by window + +Usage: + python show_convergence.py --run-dir path/to/run_folder + python show_convergence.py --run-dir path/to/run_folder --animate + python show_convergence.py --run-dir path/to/run_folder --animate --fps 24 --orbit-frames 72 + +Requirements: + pip install napari[all] h5py imageio imageio-ffmpeg +""" + +import argparse +from pathlib import Path + +import h5py +import numpy as np + + +# ── Data loading ────────────────────────────────────────────────────────────── + +def load_run(run_dir: Path) -> dict: + hdf = run_dir / "napari_data.h5" + if not hdf.exists(): + raise FileNotFoundError( + f"{hdf} not found.\n" + "Export first:\n" + " julia --project=. visualisation/napari/export_for_napari.jl " + f"--run-dir={run_dir}" + ) + + data = {} + with h5py.File(hdf, "r") as f: + for key in ("sos", "x_mm", "y_mm", "z_mm", + "source_depth_mm", "source_y_mm", "source_z_mm", + "truth_mask"): + data[key] = f[key][:] + + data["receiver_row"] = int(f["receiver_row"][()]) + + if "centerline_ids" in f: + data["cl_depth"] = f["centerline_depth_mm"][:] + data["cl_y"] = f["centerline_y_mm"][:] + data["cl_z"] = f["centerline_z_mm"][:] + data["cl_ids"] = f["centerline_ids"][:] + + keys = sorted(k for k in f if k.startswith("window_")) + data["windows"] = [f[k][:] for k in keys] + + return data + + +# ── Cumulative stack ────────────────────────────────────────────────────────── + +def precompute_cumulatives(windows: list) -> np.ndarray: + """Return shape (n_windows, nx, ny, nz) of normalized cumulative means.""" + n = len(windows) + if n == 0: + raise ValueError("No windows found in the data file.") + shape = windows[0].shape + result = np.zeros((n, *shape), dtype=np.float32) + cumul = np.zeros(shape, dtype=np.float64) + for i, w in enumerate(windows): + cumul += w.astype(np.float64) + mean = cumul / (i + 1) + vmax = float(mean.max()) + result[i] = (mean / vmax if vmax > 0 else mean).astype(np.float32) + return result + + +# ── Napari layer setup ──────────────────────────────────────────────────────── + +def setup_viewer(data: dict, cumulatives: np.ndarray): + """ + Build the napari viewer. + + cumulatives: (n_windows, nx, ny, nz) — added as a 4-D HASA layer so + napari shows a window-index slider in interactive mode. + """ + import napari + + x_mm = data["x_mm"] + y_mm = data["y_mm"] + z_mm = data["z_mm"] + + dx = float(np.diff(x_mm).mean()) + dy = float(np.diff(y_mm).mean()) + dz = float(np.diff(z_mm).mean()) + scale = (dx, dy, dz) + translate = (x_mm[0], y_mm[0], z_mm[0]) + + viewer = napari.Viewer(ndisplay=3, title="PAM window convergence") + + # ── SOS / skull ────────────────────────────────────────────────────────── + viewer.add_image( + data["sos"], + name="skull (SOS)", + scale=scale, + translate=translate, + colormap="gray", + contrast_limits=[1480.0, 2600.0], + gamma=0.7, + opacity=0.35, + blending="additive", + ) + + # ── Cumulative HASA — 4-D: (window, depth, y, z) ───────────────────────── + # Axis-0 scale=1.0 puts the window-index slider in screen units. + # The spatial axes (1,2,3) use the same mm scale as every other layer. + hasa_layer = viewer.add_image( + cumulatives, + name="cumulative HASA", + scale=(1.0, dx, dy, dz), + translate=(0.0, *translate), + colormap="inferno", + rendering="translucent", + contrast_limits=[0.0, 1.0], + opacity=0.9, + blending="additive", + ) + + # ── Truth mask ─────────────────────────────────────────────────────────── + viewer.add_image( + data["truth_mask"].astype(np.float32), + name="truth mask", + scale=scale, + translate=translate, + colormap="cyan", + rendering="iso", + iso_threshold=0.5, + opacity=0.25, + blending="additive", + ) + + # ── Source ground-truth points ──────────────────────────────────────────── + rr = data["receiver_row"] + x0 = x_mm[rr] + src_pts = np.column_stack([ + x0 + data["source_depth_mm"], + data["source_y_mm"], + data["source_z_mm"], + ]) + viewer.add_points( + src_pts, + name="truth sources", + size=1.5, + face_color="red", + symbol="cross", + blending="translucent", + ) + + # ── Vascular network centerlines ───────────────────────────────────────── + if "cl_ids" in data: + ids = data["cl_ids"] + coords = np.column_stack([ + x0 + data["cl_depth"], + data["cl_y"], + data["cl_z"], + ]) + tracks = np.column_stack([ids, coords]) + viewer.add_tracks( + tracks, + name="vascular network", + tail_width=2, + tail_length=len(tracks), + colormap="gray", + blending="additive", + ) + + # ── Receiver plane ─────────────────────────────────────────────────────── + y_ext = [y_mm[0], y_mm[-1]] + z_ext = [z_mm[0], z_mm[-1]] + plane_corners = np.array([ + [x0, y_ext[0], z_ext[0]], + [x0, y_ext[0], z_ext[1]], + [x0, y_ext[1], z_ext[1]], + [x0, y_ext[1], z_ext[0]], + ]) + viewer.add_shapes( + [plane_corners], + shape_type="polygon", + name="receiver array", + face_color=[1.0, 0.0, 1.0, 0.15], + edge_color=[1.0, 0.0, 1.0, 0.8], + edge_width=0.5, + ) + + viewer.camera.angles = (0.0, -35.0, 45.0) + viewer.camera.zoom = 1.8 + + return viewer, hasa_layer + + +# ── Animation ───────────────────────────────────────────────────────────────── + +def animate(viewer, hasa_layer, run_dir: Path, + fps: int = 24, orbit_frames: int = 72): + """ + Two-phase cinematic animation saved to napari_cinematic.mp4. + + Phase 1 — skull orbit: + HASA layer hidden; camera rotates 360° so the viewer can appreciate the + 3-D skull geometry and vascular network before any reconstruction is shown. + + Phase 2 — convergence build-up: + Camera locks at a fixed view angle; the window-index slider steps through + each cumulative HASA volume one frame at a time. + """ + import imageio + + frames_dir = run_dir / "napari_frames" + frames_dir.mkdir(exist_ok=True) + + n_windows = hasa_layer.data.shape[0] + paths = [] + fnum = 0 + + def snap(title: str): + nonlocal fnum + viewer.title = title + img = viewer.screenshot(canvas_only=True, flash=False) + p = frames_dir / f"frame_{fnum:04d}.png" + imageio.imwrite(p, img) + paths.append(p) + fnum += 1 + + # ── Phase 1: orbit ──────────────────────────────────────────────────────── + print(f" phase 1: skull orbit ({orbit_frames} frames)") + hasa_layer.visible = False + elev = -25.0 + for i in range(orbit_frames): + azimuth = (i / orbit_frames) * 360.0 - 90.0 + viewer.camera.angles = (elev, azimuth, 0.0) + snap(f"skull overview — {i + 1}/{orbit_frames}") + + # ── Phase 2: HASA convergence ───────────────────────────────────────────── + print(f" phase 2: HASA convergence ({n_windows} windows)") + hasa_layer.visible = True + viewer.camera.angles = (elev, 45.0, 0.0) + viewer.camera.zoom = 2.0 + + for i in range(n_windows): + step = list(viewer.dims.current_step) + step[0] = i + viewer.dims.current_step = tuple(step) + snap(f"window convergence — {i + 1}/{n_windows}") + + # ── Encode ──────────────────────────────────────────────────────────────── + mp4 = run_dir / "napari_cinematic.mp4" + print(f" encoding {len(paths)} frames → {mp4}") + with imageio.get_writer(str(mp4), fps=fps) as writer: + for p in paths: + writer.append_data(imageio.imread(str(p))) + + print(f"Saved: {mp4}") + + +# ── Entry point ─────────────────────────────────────────────────────────────── + +def main(): + parser = argparse.ArgumentParser(description="napari PAM convergence viewer") + parser.add_argument("--run-dir", required=True, + help="Path to window convergence run folder") + parser.add_argument("--animate", action="store_true", + help="Generate two-phase cinematic animation instead of interactive view") + parser.add_argument("--fps", type=int, default=24, + help="Animation frame rate (default: 24)") + parser.add_argument("--orbit-frames", type=int, default=72, + help="Number of frames for the skull-orbit intro (default: 72 = 3 s at 24 fps)") + args = parser.parse_args() + + run_dir = Path(args.run_dir) + data = load_run(run_dir) + + print(f"Precomputing {len(data['windows'])} cumulative windows...") + cumulatives = precompute_cumulatives(data["windows"]) + print(f" stack shape: {cumulatives.shape} ({cumulatives.nbytes / 1e9:.2f} GB)") + + viewer, hasa_layer = setup_viewer(data, cumulatives) + + if args.animate: + animate(viewer, hasa_layer, run_dir, + fps=args.fps, orbit_frames=args.orbit_frames) + else: + import napari + napari.run() + + +if __name__ == "__main__": + main() diff --git a/visualisation/wavefront_aberration.mp4 b/visualisation/wavefront_aberration.mp4 new file mode 100644 index 0000000..aab4414 Binary files /dev/null and b/visualisation/wavefront_aberration.mp4 differ diff --git a/visualisation/wavefront_aberration.py b/visualisation/wavefront_aberration.py new file mode 100644 index 0000000..7d53525 --- /dev/null +++ b/visualisation/wavefront_aberration.py @@ -0,0 +1,458 @@ +#!/usr/bin/env python3 +""" +wavefront_aberration.py +======================= +Single Ricker impulse through water vs real skull CT slice — animated using +k-Wave's PML solver (kspaceFirstOrder-OMP) for clean, reflection-free boundaries. + +Run: + "/Users/vm/INI_code/Julia II/.CondaPkg/.pixi/envs/default/bin/python3" \ + wavefront_aberration.py +""" + +import os, shutil, time, tempfile +import numpy as np +import matplotlib +matplotlib.use("Agg") +matplotlib.rcParams["animation.ffmpeg_path"] = "/opt/homebrew/bin/ffmpeg" +import matplotlib.pyplot as plt +import matplotlib.animation as animation +import matplotlib.patches as mpatches +from matplotlib.gridspec import GridSpec +from matplotlib.colors import LinearSegmentedColormap +from scipy.ndimage import zoom +import pydicom + +from kwave.kgrid import kWaveGrid +from kwave.kmedium import kWaveMedium +from kwave.ksource import kSource +from kwave.ksensor import kSensor +from kwave.kspaceFirstOrder2D import kspaceFirstOrder2D +from kwave.options.simulation_options import SimulationOptions +from kwave.options.simulation_execution_options import SimulationExecutionOptions + +# ── Physical constants ──────────────────────────────────────────────────────── +C0 = 1500.0 +C_BONE = 4500#2500.0 +RHO0 = 1000.0 +RHO_BONE = 2800.0#2100 +HU_THR = 200 + +# ── Grid ───────────────────────────────────────────────────────────────────── +LX, LY = 0.100, 0.056 # 100 mm × 76 mm +DX = 3e-4 # 0.3 mm → PPW_water=6.25 at 800 kHz +NX = int(round(LX / DX)) # 333 +NY = int(round(LY / DX)) # 253 + +x_m = np.arange(NX) * DX +y_m = np.arange(NY) * DX + +SRC_IX = NX // 2 # lateral centre (166) +SRC_IY = int(round(0.04 / DX)) # 40 mm depth (200) + +N_SEN = 16 +SEN_IX = np.linspace(NX // 6, 5 * NX // 6, N_SEN, dtype=int) + +F0 = 500_000.0 # 800 kHz — ~1.9 λ of differential delay +PML_SIZE = 20 +N_SKIP = 4 +THRESH = 3.0 + +# ── HU → (c, rho) — port of src/focus/medium.jl ────────────────────────────── +def hu_to_rho_c(hu: np.ndarray): + hu = np.clip(hu, -1000, 3000).astype(np.float32) + mask = hu >= HU_THR + c = np.full_like(hu, C0) + rho = np.full_like(hu, RHO0) + if mask.any(): + bone = hu[mask] + h_max = float(np.percentile(bone, 99.5)) + psi = np.clip((h_max - bone) / max(h_max, 1.0), 0.0, 1.0) + c[mask] = C0 + (C_BONE - C0) * (1.0 - psi) + rho[mask] = RHO0 + (RHO_BONE - RHO0) * (1.0 - psi) + return c, rho + +# ── CT loading — mirrors make_pam_medium slice extraction ──────────────────── +CT_PATH = ( + "/Users/vm/INI_code/Ultrasound/" + "DIRU_20240404_human_skull_phase_correction_1_2_(skull_Normal)/" + "DICOM/PAT_0000/STD_0000/SER_0002/OBJ_0001" +) +SLICE_IDX = 250 + +def load_skull_maps(): + print("Sorting DICOM headers …", flush=True) + t0 = time.time() + raw = [f for f in os.listdir(CT_PATH) if not f.startswith(".")] + fz = [] + for fname in raw: + ds = pydicom.dcmread(os.path.join(CT_PATH, fname), stop_before_pixels=True) + fz.append((float(ds.ImagePositionPatient[2]), fname)) + fz.sort() + print(f" {len(fz)} slices sorted ({time.time()-t0:.1f} s)", flush=True) + + target = fz[SLICE_IDX][1] + print(f" Loading slice {SLICE_IDX} z={fz[SLICE_IDX][0]:.0f} mm", flush=True) + ds = pydicom.dcmread(os.path.join(CT_PATH, target)) + hu = ds.pixel_array.astype(np.float32) + float(getattr(ds, "RescaleIntercept", -1024)) + dx_ct = float(ds.PixelSpacing[1]) * 1e-3 + n_rows, n_cols = hu.shape + print(f" Shape {hu.shape} dx={dx_ct*1e3:.2f} mm HU [{hu.min():.0f},{hu.max():.0f}]", + flush=True) + + # Orient: skull outer surface at low row index (near transducer) + bone_per_row = (hu > HU_THR).sum(axis=1) + significant = np.where(bone_per_row > 30)[0] + if len(significant) == 0: + raise RuntimeError("No skull found in this slice.") + y_top = int(significant[0]) + if y_top > n_rows // 2: + hu = hu[::-1, :] + significant = n_rows - 1 - significant + y_top = int(significant.min()) + + x_ctr = n_cols // 2 + n_above_ct = int(round(0.020 / dx_ct)) # 30 mm water pad above skull + ny_ct = int(round(LY / dx_ct)) + nx_ct = int(round(LX / dx_ct)) + y0 = y_top - n_above_ct; y1 = y0 + ny_ct + x0 = x_ctr - nx_ct // 2; x1 = x0 + nx_ct + + pad = [(max(0, -y0), max(0, y1 - n_rows)), + (max(0, -x0), max(0, x1 - n_cols))] + y0 = max(0, y0); y1 = min(n_rows, y1) + x0 = max(0, x0); x1 = min(n_cols, x1) + region = hu[y0:y1, x0:x1] + if any(v for pp in pad for v in pp): + region = np.pad(region, pad, constant_values=0.0) + + # Resample to (NX, NY) FDTD grid + hu_2d = zoom(region.astype(np.float32), + (NY / region.shape[0], NX / region.shape[1]), + order=1).T # → (NX, NY): c[ix, iy] + + # hu_2d = _flatten_skull_slab(hu_2d) + c_map, rho_map = hu_to_rho_c(hu_2d) + print(f" Bone fraction in domain: {(c_map > C0+20).mean()*100:.1f}%", flush=True) + return c_map, rho_map, hu_2d + + +def _flatten_skull_slab(hu_2d: np.ndarray) -> np.ndarray: + """ + Remap the curved skull arch to a flat horizontal slab. + + For each lateral column (ix), the bone pixels (HU ≥ HU_THR) are extracted + and resampled into a uniform depth band whose position and thickness are the + median across all columns. This ensures every wavefront path — centre or + edge — travels through the same bone depth, making aberration visible across + the full aperture. Real HU heterogeneity (diploë / cortical layers) is + preserved within each column. + """ + nx = hu_2d.shape[0] + bone_mask = hu_2d >= HU_THR + + col_top = np.full(nx, -1, dtype=int) + col_bot = np.full(nx, -1, dtype=int) + for ix in range(nx): + rows = np.where(bone_mask[ix, :])[0] + if len(rows) >= 2: + col_top[ix] = rows[0] + col_bot[ix] = rows[-1] + + valid = col_top >= 0 + if valid.sum() < nx // 4: + return hu_2d # not enough skull columns — leave untouched + + slab_top = int(np.median(col_top[valid])) + slab_thick = int(round(0.020 / DX)) # force 20 mm slab (~1.9 λ of differential delay) + + hu_flat = np.zeros_like(hu_2d) # 0 HU = water everywhere + for ix in range(nx): + if not valid[ix]: + continue + col = hu_2d[ix, col_top[ix]:col_bot[ix] + 1] + if len(col) != slab_thick: + col = zoom(col.astype(np.float32), slab_thick / len(col), order=1) + hu_flat[ix, slab_top:slab_top + slab_thick] = col[:slab_thick] + + print(f" Skull slab: top={slab_top} ({slab_top*DX*1e3:.1f} mm) " + f"thick={slab_thick} ({slab_thick*DX*1e3:.1f} mm)", flush=True) + return hu_flat + +# ── k-Wave simulation ───────────────────────────────────────────────────────── +# Use C_BONE to fix DT across both simulations so snapshots align in time. +T_END = 1.5 * SRC_IY * DX / C0 # ≈ 60 µs + +def simulate_kwave(c_map: np.ndarray, rho_map: np.ndarray, label: str): + print(f"\nRunning k-Wave ({label}) …", flush=True) + t0 = time.time() + + kgrid = kWaveGrid([NX, NY], [DX, DX]) + kgrid.makeTime(C_BONE, cfl=0.3, t_end=T_END) # fixes DT for both sims + NT = int(kgrid.Nt) + DT = float(kgrid.dt) + print(f" Grid {NX}×{NY} NT={NT} DT={DT*1e9:.1f} ns " + f"T_end={NT*DT*1e6:.0f} µs λ_water={C0/F0*1e3:.2f} mm " + f"PPW_water={C0/F0/DX:.1f}", flush=True) + + medium = kWaveMedium( + sound_speed=c_map.astype(np.float32), + density=rho_map.astype(np.float32), + ) + + # Ricker wavelet: broadband single impulse + t_v = np.arange(NT) * DT + xi = np.pi * F0 * (t_v - 1.5 / F0) + pulse = (1.0 - 2.0 * xi**2) * np.exp(-xi**2) * 3500.0 + + source = kSource() + p_mask = np.zeros((NX, NY), dtype=bool) + p_mask[SRC_IX, SRC_IY] = True + source.p_mask = p_mask + source.p = pulse.reshape(1, -1).astype(np.float64) + source.p_frequency_ref = F0 + source.medium = medium + + # Full-field sensor (all NX×NY cells at every timestep) + sensor = kSensor() + sensor.mask = np.ones((NX, NY), dtype=bool) + sensor.record = ["p"] + + sim_dir = tempfile.mkdtemp() + try: + data = kspaceFirstOrder2D( + kgrid=kgrid, + medium=medium, + source=source, + sensor=sensor, + simulation_options=SimulationOptions( + pml_inside=False, + pml_size=PML_SIZE, + data_cast="single", # float32 throughout + data_recast=False, + save_to_disk=True, + data_path=sim_dir, + ), + execution_options=SimulationExecutionOptions( + is_gpu_simulation=False, + delete_data=True, + ), + ) + finally: + shutil.rmtree(sim_dir, ignore_errors=True) + + # data["p"] shape: (NT, NX*NY), Fortran-ordered spatially (x varies fastest) + p_raw = np.array(data["p"]) # float32, (NT, NX*NY) + p_3d = p_raw.T.reshape((NX, NY, NT), order="F") # (NX, NY, NT) + del p_raw + + # Full RF time series at each sensor (transducer row iy=0) + rf_data = p_3d[SEN_IX, 0, :].copy() # (N_SEN, NT) + + # Subsample snapshots for animation + snap_idx = np.arange(0, NT, N_SKIP) + snaps = [p_3d[:, :, i].copy() for i in snap_idx] + del p_3d + + print(f" Done in {time.time()-t0:.1f} s", flush=True) + return snaps, rf_data, NT, DT + +# ── Load CT and run both simulations ───────────────────────────────────────── +print("Loading CT skull …") +c_skull, rho_skull, hu_map = load_skull_maps() +c_water = np.full((NX, NY), C0, dtype=np.float32) +rho_water = np.full((NX, NY), RHO0, dtype=np.float32) + +snaps_w, rf_w, NT, DT = simulate_kwave(c_water, rho_water, "water") +snaps_s, rf_s, _, _ = simulate_kwave(c_skull, rho_skull, "skull") + +N_FR = min(len(snaps_w), len(snaps_s)) +T_US = np.arange(NT) * DT * 1e6 # time axis in µs (length NT) +print(f"\nFrames: {N_FR} ({N_FR/25:.1f} s @ 25 fps)") + +# ── Figure setup ───────────────────────────────────────────────────────────── +BG = "#070a0f"; FG = "#a8bbc8" + +fig = plt.figure(figsize=(16, 10), facecolor=BG) +gs = GridSpec(2, 2, + height_ratios=[3.2, 1.0], + hspace=0.08, wspace=0.09, + left=0.06, right=0.96, top=0.92, bottom=0.06) +ax_w = fig.add_subplot(gs[0, 0]) +ax_s = fig.add_subplot(gs[0, 1]) +ax_bw = fig.add_subplot(gs[1, 0]) +ax_bs = fig.add_subplot(gs[1, 1]) + +for ax in (ax_w, ax_s, ax_bw, ax_bs): + ax.set_facecolor(BG) + for sp in ax.spines.values(): + sp.set_edgecolor("#18263a") + ax.tick_params(colors=FG, labelsize=8) + +# Pressure colormap: black at 0, blue for rarefaction, red for compression +_cd = { + "red": [(0, .04, .04), (0.5, 0., 0.), (1, .95, .95)], + "green": [(0, .14, .14), (0.5, 0., 0.), (1, .10, .10)], + "blue": [(0, .90, .90), (0.5, 0., 0.), (1, .06, .06)], +} +PCMAP = LinearSegmentedColormap("glow", _cd) +VMAX = 70.0 + +X_MM = x_m * 1e3; Y_MM = y_m * 1e3 +EXT = [X_MM[0], X_MM[-1], Y_MM[-1], Y_MM[0]] + +# ── Skull overlay (real c-map coloured by sound speed) ─────────────────────── +skull_mask = c_skull > C0 + 20.0 +sm_T = skull_mask.T +c_skull_T = c_skull.T +c_in = c_skull_T[sm_T] +c_min = float(c_in.min()) if c_in.size else C0 +c_max = float(c_in.max()) if c_in.size else C_BONE +c_norm_T = np.zeros_like(c_skull_T) +if c_in.size: + c_norm_T[sm_T] = (c_skull_T[sm_T] - c_min) / max(c_max - c_min, 1.0) + +bone_cm = plt.cm.YlOrBr +skull_rgba = np.zeros((NY, NX, 4), dtype=np.float32) +if sm_T.any(): + rgba = bone_cm(c_norm_T[sm_T]) + skull_rgba[sm_T, :3] = rgba[:, :3] + skull_rgba[sm_T, 3] = 0.65 +water_rgba = np.zeros((NY, NX, 4), dtype=np.float32) + +def arch_boundaries(): + outer = np.full(NX, np.nan); inner = np.full(NX, np.nan) + for ix in range(NX): + idx = np.where(skull_mask[ix])[0] + if len(idx): + outer[ix] = Y_MM[idx[0]]; inner[ix] = Y_MM[idx[-1]] + return outer, inner + +outer_mm, inner_mm = arch_boundaries() + +def setup_wave_ax(ax, title, tcol, overlay, show_skull_outline): + im = ax.imshow( + np.zeros((NY, NX)), origin="upper", + extent=EXT, cmap=PCMAP, vmin=-VMAX, vmax=VMAX, + aspect="equal", interpolation="bilinear", zorder=1, + ) + ax.imshow(overlay, origin="upper", extent=EXT, + aspect="equal", interpolation="nearest", zorder=2) + if show_skull_outline: + ax.plot(X_MM, outer_mm, color="#886600", lw=0.9, ls="--", zorder=3, alpha=0.75) + ax.plot(X_MM, inner_mm, color="#886600", lw=0.9, ls="--", zorder=3, alpha=0.75) + ax.scatter(X_MM[SEN_IX], np.ones(N_SEN) * Y_MM[1], + s=30, c="#22ff88", marker="v", zorder=6, lw=0) + ax.scatter([X_MM[SRC_IX]], [Y_MM[SRC_IY]], + s=100, c="white", marker="*", zorder=6, lw=0) + ax.set_xlim(X_MM[0], X_MM[-1]); ax.set_ylim(Y_MM[-1], Y_MM[0]) + ax.set_xlabel("Lateral [mm]", color=FG, fontsize=9) + ax.set_ylabel("Depth [mm]", color=FG, fontsize=9) + ax.set_title(title, color=tcol, fontsize=14, fontweight="bold", pad=7) + return im + +im_w = setup_wave_ax(ax_w, "Water only", "#5599ff", water_rgba, show_skull_outline=False) +im_s = setup_wave_ax(ax_s, "With skull", "#ff8844", skull_rgba, show_skull_outline=True) + +# Pressure legend (blue = rarefaction, red = compression) +_leg = ax_w.legend( + handles=[ + mpatches.Patch(color="#0a22e6", label="Rarefaction (−p)"), + mpatches.Patch(color="#e61a0a", label="Compression (+p)"), + ], + loc="lower left", fontsize=7.5, framealpha=0.45, + facecolor=BG, edgecolor="#18263a", labelcolor=FG, +) +ax_w.add_artist(_leg) + +ax_w.text(X_MM[-1]-1, Y_MM[1]+0.5, "Sensors", + color="#22ff88", fontsize=7.5, ha="right", va="bottom", zorder=7) +mid_mm = float(np.nanmean([outer_mm[NX//2], inner_mm[NX//2]])) \ + if not np.isnan(outer_mm[NX//2]) else 35 + +from mpl_toolkits.axes_grid1 import make_axes_locatable +_div = make_axes_locatable(ax_s) +_cax = _div.append_axes("right", size="2.5%", pad=0.04) +_sm = plt.cm.ScalarMappable(cmap=bone_cm, norm=plt.Normalize(c_min, c_max)) +_cb = fig.colorbar(_sm, cax=_cax) +_cb.set_label("c [m/s]", color=FG, fontsize=8) +_cb.ax.tick_params(colors=FG, labelsize=7); _cb.outline.set_edgecolor("#18263a") + +# ── RF wiggle traces ────────────────────────────────────────────────────────── +# Normalise both panels identically so water/skull amplitudes are comparable. +rf_scale = max(np.abs(rf_w).max(), np.abs(rf_s).max(), 1.0) / 0.65 + +def setup_rf_ax(ax, color, label): + ax.set_facecolor("#0b1520") + for sp in ax.spines.values(): sp.set_edgecolor("#18263a") + ax.tick_params(colors=FG, labelsize=7) + + # Faint zero-baseline rule for each sensor + for s in range(N_SEN): + ax.axhline(s, color="#18263a", lw=0.5, zorder=1) + + # One line object per sensor (data filled in during animation) + lines = [ax.plot([], [], color=color, lw=0.75, alpha=0.9, zorder=3)[0] + for _ in range(N_SEN)] + + # Vertical time cursor + cursor, = ax.plot([], [], color=FG, lw=0.8, ls="--", alpha=0.35, zorder=5) + + ax.set_xlim(0, T_US[-1]) + ax.set_ylim(-0.6, N_SEN - 0.4) + ax.set_xlabel("Time [µs]", color=FG, fontsize=9) + + tick_s = [0, 4, 8, 12, N_SEN - 1] + ax.set_yticks(tick_s) + ax.set_yticklabels([f"{X_MM[SEN_IX[s]]:.0f} mm" for s in tick_s], + fontsize=6.5, color=FG) + ax.set_ylabel("Sensor lateral pos.", color=FG, fontsize=8) + ax.set_title(f"RF traces — {label}", color=color, fontsize=9.5, pad=3) + return lines, cursor + +lines_w, cursor_w = setup_rf_ax(ax_bw, "#4488ee", "water only") +lines_s, cursor_s = setup_rf_ax(ax_bs, "#ff7744", "skull in water") + +fig.suptitle( + f"Transcranial Ultrasound — Single Ricker Impulse ({F0/1e3:.0f} kHz) " + f"Through a Real Skull [CT slice {SLICE_IDX}]", + color="white", fontsize=12.5, fontweight="bold", y=0.97, +) +t_label = fig.text(0.5, 0.935, "t = 0.0 µs", + ha="center", color=FG, fontsize=11) + +# ── Animation ───────────────────────────────────────────────────────────────── +def update(fr): + n = min(fr * N_SKIP, NT - 1) + t_us = T_US[n] + t_label.set_text(f"t = {t_us:.1f} µs") + im_w.set_data(snaps_w[fr].T) + im_s.set_data(snaps_s[fr].T) + + t_slice = T_US[:n + 1] + for s in range(N_SEN): + lines_w[s].set_data(t_slice, rf_w[s, :n + 1] / rf_scale + s) + lines_s[s].set_data(t_slice, rf_s[s, :n + 1] / rf_scale + s) + cursor_w.set_data([t_us, t_us], [-0.6, N_SEN - 0.4]) + cursor_s.set_data([t_us, t_us], [-0.6, N_SEN - 0.4]) + + return [im_w, im_s, *lines_w, *lines_s, cursor_w, cursor_s, t_label] + +ani = animation.FuncAnimation(fig, update, frames=N_FR, interval=40, blit=True) + +out_dir = os.path.dirname(os.path.abspath(__file__)) +out_mp4 = os.path.join(out_dir, "wavefront_aberration.mp4") +out_gif = os.path.join(out_dir, "wavefront_aberration.gif") + +try: + print(f"\nSaving {out_mp4} …") + ani.save(out_mp4, + writer=animation.FFMpegWriter(fps=75, bitrate=4000, + extra_args=["-pix_fmt", "yuv420p"])) + print(f"Saved: {out_mp4}") +except Exception as e: + print(f"FFMpeg failed ({e}), saving GIF …") + ani.save(out_gif, writer=animation.PillowWriter(fps=20)) + print(f"Saved: {out_gif}") + +plt.close(fig)