From edda5fc4f8c46bea1df08c4eda1f0205a4005452 Mon Sep 17 00:00:00 2001 From: Mashy Green Date: Mon, 17 Jun 2024 13:03:12 +0100 Subject: [PATCH 1/2] #125: Added ElasticMedium class and a unit test for it --- +kwave/+tests/+unit/TestElasticMedium.m | 23 ++++++++ .../+toolbox/@ElasticMedium/ElasticMedium.m | 56 +++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 +kwave/+tests/+unit/TestElasticMedium.m create mode 100644 +kwave/+toolbox/@ElasticMedium/ElasticMedium.m diff --git a/+kwave/+tests/+unit/TestElasticMedium.m b/+kwave/+tests/+unit/TestElasticMedium.m new file mode 100644 index 00000000..33aa62c0 --- /dev/null +++ b/+kwave/+tests/+unit/TestElasticMedium.m @@ -0,0 +1,23 @@ +%% TestElasticMedium +% *Package:* kwave.tests.unit +% *Superclasses:* kwave.tests.unit.AbstractTestGridInput +% +% Unit tests for the ElasticMedium class using the TestMedium class. + +classdef TestElasticMedium < kwave.tests.unit.AbstractTestGridInput + + properties + inputClass = 'ElasticMedium' + inputProperties = {'density', 'soundSpeedCompression', 'soundSpeedShear', ... + 'alphaCoeffCompression', 'alphaCoeffShear'} + inputPropertiesPadded = {'densityPadded', 'soundSpeedCompressionPadded', 'soundSpeedShearPadded', ... + 'alphaCoeffCompressionPadded', 'alphaCoeffShearPadded'} + inputPropertiesScalar = {} + inputPropertiesComplex = {} + inputPropertiesVectorField = {} + end + + methods(Test) + end + +end diff --git a/+kwave/+toolbox/@ElasticMedium/ElasticMedium.m b/+kwave/+toolbox/@ElasticMedium/ElasticMedium.m new file mode 100644 index 00000000..9450353d --- /dev/null +++ b/+kwave/+toolbox/@ElasticMedium/ElasticMedium.m @@ -0,0 +1,56 @@ +%% ElasticMedium +% *Package:* kwave.toolbox +% *Superclasses:* kwave.toolbox.GridInput +% +% Class used to define the elastic medium properties for a simulation. +% +%% Syntax +% medium = ElasticMedium(kgrid); +% +%% Description +% This class is used to define the elastic medium properties. The +% constructor takes an object of the |kwave.toolbox.Grid| class which +% defines the grid size. All medium properties can be scalar or have +% the same size as the grid. The |soundSpeedCompression|, +% |soundSpeedShear| and |density| must be defined, while the +% |soundSpeedCompression| and |soundSpeedShear| are used only with the +% Kelvin-Voigt model. +% +%% Examples +% Define the grid and medium objects, and assign the sound speed and +% density. +% +% kgrid = kwave.toolbox.Grid([128, 128], 1e-3); +% medium = kwave.toolbox.ElasticMedium(kgrid); +% medium.soundSpeedShear = rand(medium.gridSize); +% medium.soundSpeedCompression = rand(medium.gridSize); +% medium.density = rand(medium.gridSize); +% +%% Properties +% * |density| - (single) Mass density [kg/m^2]. +% * |soundSpeedCompression| - (single) Compressional sound speed +% * distribution within the acoustic medium [m/s]. +% * |soundSpeedShear| - (single) Shear sound speed distribution within +% * the acoustic medium [m/s]. +% * |alphaCoeffCompression| - (single) Power law absorption coefficient +% * for compressional waves [dB/(MHz^2 cm)] +% * |alphaCoeffShear| - (single) Power law absorption coefficient for +% * shear waves [dB/(MHz^2 cm)] +% +%% See Also +% * |GridInput| + +classdef ElasticMedium < kwave.toolbox.GridInput + + properties(Constant, Hidden=true) + requiredProperties = {'density', 'soundSpeedCompression', 'soundSpeedShear'}; + gridFields = kwave.toolbox.GridField.createGridFieldsMap([ + kwave.toolbox.GridField('density'); + kwave.toolbox.GridField('soundSpeedCompression'); + kwave.toolbox.GridField('soundSpeedShear'); + kwave.toolbox.GridField('alphaCoeffCompression'); + kwave.toolbox.GridField('alphaCoeffShear'); + ]); + end + +end From 9b9ea0ffbf1b52d54b2b0e1a22a4f685796b4e59 Mon Sep 17 00:00:00 2001 From: Stella Prins <30465823+stellaprins@users.noreply.github.com> Date: Thu, 28 May 2026 12:08:32 +0100 Subject: [PATCH 2/2] update branch and reformat files using pre-commit (#242) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * #49: Work in progress to save .m files to .md * #49: Removes folder helpfilesweb from tracking * #49: Clean-up function mToMarkdown * #49: Change mToMarkdown input from namespace name to filename with full file path * #126: Added ElasticSource class and a unit test for it * First attempt at mkdocs. Requires the literate-nav plugin. * Generate md the same way as html. mToMarkdown is not used anymore. * Fix bug if duplicated unit test docs files. Some refactoring. * Fix documentation files titles. * Generate sections for markdown toc by creating subfolders and SUMMARY.md files. * Evaluate and show the code when converting to md as well (same as html). * Move static documentation out of .m files and into md * Clean up tutorials * #147: Fix all broken links and matlab markup syntax errors in all .m docs. * Apply suggestions from code review Co-authored-by: Devaraj Gopinathan <36169767+Devaraj-G@users.noreply.github.com> * #147: Address code review comments: Rename generateHTML function, since now it does more than generating html. Move link fixing to its own function, to de-clutter generateDocFiles function a bit. * Add the Template Class documentation to the html toc. * #156: remove helpfiles subdir - to be re-generated on the fly, not commited to the repo. * #156: add some static md files in docs, and clean up the folder. * #156: add placeholder for helpfilesweb folder and softlink for README * #156: Update instructions on generating documentation. * #156: Fix LaTeX rendering * #156: Tidy up folders for generated documentation. * #156: Move instructions for building the docs into the developer docs only. * #156: Try to make the GA runner pick up the latest release of matlab... * #156: Try to make the GA runner pick up the latest release of matlab - attempt 2... * #156: Try to make the GA runner pick up the latest release of matlab - attempt 3... * #156: Try to make the GA runner pick up the latest release of matlab - attempt 4... * #156: Try to make the GA runner pick up the latest release of matlab - attempt 5... * #156: Try to make the GA runner pick up the latest release of matlab - attempt 6... * #156: Try to make the GA runner pick up the latest release of matlab - attempt 7... * #156: Save docs files as artifacts in runner * #156: Save docs files as artifacts in runner - attempt 2 * #156: Save docs files as artifacts in runner - attempt 3 * #156: Save docs files as artifacts in runner - attempt 4 * #156: Save docs files as artifacts in runner - attempt 5 * #156: Save docs files as artifacts in runner - attempt 6 * #156: Save docs files as artifacts in runner - attempt 7 * #156: Save docs files as artifacts in runner - attempt 8 * #156: Save docs files as artifacts in runner - attempt 9 * #156: Save docs files as artifacts in runner - attempt 10 * #156: Save docs files as artifacts in runner - attempt 11 * #156: put all python package requirements into requirements.txt * #156: write test to count docs files and run in GA * #156: try number of doc files test with powershell * #156: retry number of doc files test with python * #156: remove number of doc files test with python from workflow, but have script ready for future use. * #156: Address review comments and re-attempt python test. - Fix security warning in js package - Get order of folders in mkdocs the same as in generated matlab help page - Fix broken licence link - Re-attempt soft link to README.md file from index.md, instead of the other way round. * #156: retry python test - attempt 2. * #156: retry python test - attempt 3. * #156: use bash test instead. * #156: Re-shuffle utilities folder, separate docs of interest to the user and the developer. * #156: correct script location in GA. * #164: Separate user from contributor docs, in files and mkdocs side bar. * #164: update workflow in contributor docs, add maintainer docs placeholder. * #164: add maintainer docs. * #164: Clarify some bits in the docs after discussions. * #13: add help files for helptoc section headings. * #13: fix links in the section headings. * #13: update documentation about link fixing. * Apply suggestions from code review Co-authored-by: Ben Cox * #164: Address more review comments. * #172 Removed EM medium and source from develop branch * #132 Acoustic sensor class and tests added from MKingEndBranch * #132 Updated t02_AcousticSolverExamples.m * #132 Updated TestAcousticSensor.m * #132 Many small changes found while improving code check coverage * #132 Tidied up header text in sensor and related classes and tidied up simple tutorials * #132 Bug fix to pass tests * Stop CI from running on draft PRs (#186) * Do not run CI workflows for draft pull requests * run code_quality workflow on draft PRs * Update workflow triggers to exclude draft PRs * Also run on push (to main) * gate non-draft PR trigger through workflow_call * Revert "gate non-draft PR trigger through workflow_call" This reverts commit a313e3c80989a4e89964e52c4de81e1462cc8328. * Clarify that jobs are skipped on draft PRs * Add vscode settings folder to gitignore (#185) ignore VSCode settings * add pre-commit hooks * Add codespell, remove markdownlint-fix and forbid-tabs * Add environment setup and pre-commit instructions * Add pre-commit workflow configuration * Use ubuntu-latest in pre-commit workflow * 179: update docs that we use squash commits. (#187) * Create top level docs table of contents (#189) #37: Create top level docs toc. * Fix spelling mistakes (#194) * fix spelling mistakes * Apply suggestions from code review Some symbols are rendering strange, fix them. --------- Co-authored-by: Ilektra Christidi * add ilekra's suggestion https://github.com/ucl-bug/k-wave-ii/pull/195/changes#r2840666954 * add pre-commit to requirments.txt https://github.com/ucl-bug/k-wave-ii/pull/195/changes#r2840644366 * Add ilektra's suggestion https://github.com/ucl-bug/k-wave-ii/pull/195/changes#r2840634706 * exclude markdown from trailing whitespace checks https://github.com/ucl-bug/k-wave-ii/pull/195#discussion_r2840732760 * Sp/release (#207) * Add issue templates copied from k-wave-i * fix folder hiearchy for PR template * Test old PR template hierarchy * Add pull request template for better contributions Added a pull request template to guide contributors in providing necessary information. * Delete .github/PULL_REQUEST_TEMPLATE directory * Update to v4 and add deploy to gh pages * test gh pages deployment Replace GitHub Actions deployment step with mkdocs command. * test deploy gh pages Updated deployment steps for GitHub Pages in workflow. * Update deployment steps for GitHub Pages * test deploy docs Updated the GitHub Actions workflow for documentation generation and deployment. Added permissions, changed the runner to 'windows-latest', and modified steps for building and deploying MkDocs site. * Update branch reference from main to develop in workflow configuration * test workfow trigger * use orginal docs workflow * trigger on push to develop * Update workflow to build and deploy to gh pages * use ubuntu runner * use mathworks actions * Add semantic release configuration and workflow * Update release workflow to include npm caching and consistent versioning * Add node modules to gitignore * add package(-lock) jsons * Revert "Sp/release (#207)" This reverts commit f64df904cb3d5d08caebc3d3821c2e450d9fae6d. * Deploy the documentation to GitHub Pages (#202) * Update to v4 and add deploy to gh pages * Add GitHub Pages ("site") directory to .gitignore * Deploy when push to sp/49-deploy_gh_pages branch * add build mkdocs step to docs workflow Co-authored-by: Arindam Saha * add python setup (comment out build docs to speed up testing) Co-authored-by: Arindam Saha * change to windows-latest Co-authored-by: Arindam Saha * upload to site instead of docs Co-authored-by: Arindam Saha * Added venv to gitignore Co-authored-by: Stella Prins * Trying to deploy Co-authored-by: Stella Prins * Write permissions given? Co-authored-by: Stella Prins * Upload to github pages Co-authored-by: Stella Prins * Naming error Co-authored-by: Stella Prins * Damn you windows! Co-authored-by: Stella Prins * Added Matlab helpfile documentation Co-authored-by: Stella Prins * Testing Co-authored-by: Stella Prins * Order! Order! Co-authored-by: Stella Prins * Connect the two jobs Co-authored-by: Stella Prins * Version upgrade due to deprecation Co-authored-by: Stella Prins * Trying Co-authored-by: Stella Prins * Trying 2 Co-authored-by: Stella Prins * Trying 3 Co-authored-by: Stella Prins * Done! Removing unnecessary if statements Co-authored-by: Stella Prins * More descriptive runner name Co-authored-by: Stella Prins * Workflow to run only when of develop branch Co-authored-by: Stella Prins * Deployment skipped on PR's Co-authored-by: Stella Prins --------- Co-authored-by: Arindam Saha Co-authored-by: Arindam Saha Co-authored-by: Stella Prins * Added guidelines on use of AI Policy (#203) * Add issue templates and fix PR template hierarchy (#192) * Add issue templates copied from k-wave-i * fix folder hiearchy for PR template * Update .github/ISSUE_TEMPLATE/bug-form.yml Co-authored-by: Ilektra Christidi * remove custom issues and enable blank issues * Add ilektra's suggestion for PR checklist (to better match contribution guidelines) --------- Co-authored-by: Ilektra Christidi * Add contributor agreement (#201) * Fixes issue 178 also added the link in the PR template, did not change anything else there since it is an issue assigned to Stella * Moved legal to the end after merge removed it * #178: fix merge conflict mess * #178: remove superfluous PR template file --------- Co-authored-by: blaze Co-authored-by: Ilektra Christidi * Added a code of conduct (#198) * Added basic code of conduct * Added the page to nav bar * Better list rendering due to correct tabbing * Made it concise * Add contact information in codeOfConduct.md --------- Co-authored-by: Ilektra Christidi * remove space to fix broken badge link Co-authored-by: Arindam Saha * test branch protection (remove redundent blank line from README) Co-authored-by: Arindam Saha * test branch protection (remove redundent blank line from README) Co-authored-by: Arindam Saha * test branch protection (#213) test branch protection (remove redundent blank line from README) Co-authored-by: Arindam Saha * add contributors workflow (#210) * add contributors workflow Co-authored-by: Arindam Saha * Add Ben as project owder Co-authored-by: Arindam Saha * docs: update contributors * Add badge and lis tags to README Co-authored-by: Arindam Saha * docs: update contributors * remove bots and add co authors Co-authored-by: Arindam Saha * remove contributors Co-authored-by: Arindam Saha * remove readme tags Co-authored-by: Arindam Saha * fix indentation Co-authored-by: Arindam Saha * use scan Co-authored-by: Arindam Saha * change back to working workflow Co-authored-by: Arindam Saha * docs: update contributors * Add contributors section header Co-authored-by: Arindam Saha * docs: update contributors * exclude bots as contributors Co-authored-by: Arindam Saha * docs: update contributors * set custom type fields to null (instead of empty string) Co-authored-by: Arindam Saha * docs: update contributors * adjust custom type * docs: update contributors * remove contributorsfooter * docs: update contributors * set link to usage to false and add footer Co-authored-by: Arindam Saha * docs: update contributors * add reference to all contributors Co-authored-by: Arindam Saha * docs: update contributors * include co authors as contributors Co-authored-by: Arindam Saha * include co authors as contributors Co-authored-by: Arindam Saha * docs: update contributors * exclude bots as contributors Co-authored-by: Arindam Saha * remove bot Co-authored-by: Arindam Saha * docs: update contributors * properly install all-contributors and add contributors Co-authored-by: Arindam Saha * docs: update contributors * replace old tags in readme * docs: update contributors * add contributors Co-authored-by: Arindam Saha * fix all contributors src file Co-authored-by: Arindam Saha * docs: update contributors * clarify where the all contributors information is sourced from (GDPR) * docs: update contributors * change wording slightly * add forum contact details in case people want to request to be removed * docs: update contributors * skip commit and push if no changes in .all-contributorsrc detected * remove redundent blank lines in readme (and test whether bot commits) * add Ben's email as contact point instead of forum. * add workflow for protecting npm package jsons Co-authored-by: Arindam Saha * change workflow triggers for protecting npm package jsons Co-authored-by: Arindam Saha * break package-lock.json Co-authored-by: Arindam Saha * fix package-lock.json using `npm install --package-lock-only` sugestion Co-authored-by: Arindam Saha * try to break package.json Co-authored-by: Arindam Saha * add _comment to package json file Co-authored-by: Arindam Saha * change version of all contributors cli in package json Co-authored-by: Arindam Saha * pick better descriptive workflow name and revert previous commit Co-authored-by: Arindam Saha * Added proper link Co-authored-by: Ilektra Christidi --------- Co-authored-by: Arindam Saha Co-authored-by: github-actions[bot] Co-authored-by: Arindam Saha Co-authored-by: Ilektra Christidi * Remove `package.json` and `package-lock.json` files and workflow (#215) * take into account non-PR triggers * remove lockflow workflow * remove `package.json` and `package-lock.json` Co-authored-by: Arindam Saha * remove npm installation from contributors workflow Co-authored-by: Arindam Saha * add npm dependency (but not using npm ci) * remove Arindam from .all-contributorssrc file * update contributors --------- Co-authored-by: Arindam Saha Co-authored-by: github-actions[bot] * add ilektra's suggestion about running pre-commit on all files * add contributors (#218) * docs: update README.md * docs: update .all-contributorsrc * change code to custom for jarosjir * try to add Eleanor * add ellymartin and ellwise as contributors --------- Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: Stella <30465823+stellaprins@users.noreply.github.com> * Move README.md to docs folder (#226) * Move README.md to docs folder * Refer to docs/README.md in .all-contributorsrc and contributors workflow * Add "development lead" section to README (#222) * Update README.md to include development lead group (BUG) * Add Ben's suggested changes Co-authored-by: Ben Cox --------- Co-authored-by: Ben Cox Co-authored-by: Ilektra Christidi * Improve contributors grid (#228) * adjust .all-contributorsrc - change order profiles (Bradley first) - slightly smaller thumbnails (100->80 pixels) - 6 instead of 7 profiles per row * update contributors * change order grid * fix: correct JSON syntax in .all-contributorsrc * update contributors * add back Devaraj (accidentily deleted him!) --------- Co-authored-by: github-actions[bot] * Add contributor counter badge (#221) * Update README.md to include new contributors badge * Remove the badge that is not working * Replace deprecated `` tags (#234) * replace tt tages by code tags in published html files Co-authored-by: Arindam Saha * Replace deprecated tags with tags in generated HTML files --------- Co-authored-by: Arindam Saha * Add `CITATION.cff` (#232) * add cff file * remove hyphens from keyword labels (and use spaces instead) Treeby, B. E., Jaros, J., Rendell, A. P., & Cox, B. T. (2012). Modeling nonlinear ultrasound propagation in heterogeneous media with power law absorption using a k-space pseudospectral method. The Journal of the Acoustical Society of America, 131(6), 4324–4336. https://doi.org/10.1121/1.4712021 * #188 Fixed how tutorial docs display (#204) * #188 Fixed how tutorial docs display * #188: fix t01_AcousticSolverExamples1D to display correctly. * #188 Updated two tutorial files so they format correctly when converted to html. * #188: Fix some aesthetic things and add documentation about markup peculiarities. --------- Co-authored-by: Stella Prins <30465823+stellaprins@users.noreply.github.com> Co-authored-by: Ilektra Christidi * #142 medium class updates (#205) * #142 New Materials class for materials to be passed to the Medium class * #142 Updated Materials class definition and added unit tests * #142 Rewritten Medium class to work with Materials class object. Also Medium class unit test. * #142 Tutorial example and various debugged files that it flagged up. Deleted unused files. * #142 Bug fixes to ensure unit tests all pass * #142 Rewrote TestMedium and TestMaterials to improve unit test coverage. * #142 updated header to Materials.m so docs compile * #142 updated header to Materials.m again so docs compile * #142 Minor updates to improve test coverage of Medium and Materials classes. * #142 Updated Materials and TestMaterials to improve unit test coverage, and tighten class definition * #142 Updated Medium and TestMedium to improve coverage. Small changes to tutorial example. * #142 updates following comments, and some other largely cosmetic tidying up of the code. * fix docs error (#233) * remove (potentially) problematic | markers and replace with backticks * use disp(table) so it displays properly in the documentation * update tutorial documentation Co-authored-by: Stella Prins <30465823+stellaprins@users.noreply.github.com> Co-authored-by: Arindam Saha * Explain where to run the GenerateDocumentation script from. (#240) 193: Explain where to run the GenerateDocumentation script from. * added script and linter option for checking/adding license block (#212) * added script and linter option for checking/adding license block * #14: Move yaml configuration to workflow folder, to run as GA. Fix bug in script. * #14: Fixed some comments * #14: adjust triggers to run new action. Fix trigger for tests action. * style: automatically apply LGPL license headers to .m files * Revert "style: automatically apply LGPL license headers to .m files" This reverts commit b9398ba5ad1d8ee0f6350f010d45e1506c3f811c. * 14: Remove legacy files from copyright check. Debugging stuff still in the script. * 14: Fix search for existing wrong Copyright header. Add some verbose print statements * style: automatically apply LGPL license headers to .m files * style: automatically apply LGPL license headers to .m files * 14: Remove files brought back with the merge by mistake. --------- Co-authored-by: blaze Co-authored-by: Ilektra Christidi Co-authored-by: ilectra <22891967+ilectra@users.noreply.github.com> Co-authored-by: Stella Prins <30465823+stellaprins@users.noreply.github.com> * Update pre-commit configuration to exclude legacy files * exclude site directory and don't use prettier on docs or markdown files * run pre-commit on all files and add changes * style: automatically apply LGPL license headers to .m files * update contributors --------- Co-authored-by: Bradley Treeby Co-authored-by: Devaraj Gopinathan Co-authored-by: Mashy Green Co-authored-by: Ben Cox Co-authored-by: Ilektra Christidi Co-authored-by: Devaraj Gopinathan <36169767+Devaraj-G@users.noreply.github.com> Co-authored-by: MatthewJohnKing <137497118+MatthewJohnKing@users.noreply.github.com> Co-authored-by: Arindam Saha Co-authored-by: Arindam Saha Co-authored-by: Stella Prins Co-authored-by: nicolin Co-authored-by: blaze Co-authored-by: github-actions[bot] Co-authored-by: allcontributors[bot] <46447321+allcontributors[bot]@users.noreply.github.com> Co-authored-by: ilectra <22891967+ilectra@users.noreply.github.com> --- +kwave/+devtools/GenerateDocumentation.m | 423 +++++++++++ .../gitChangedFiles.m | 24 +- +kwave/+devtools/parseTitle.m | 45 ++ +kwave/+docfiles/+developer/codingStandard.m | 153 ---- .../+developer/developerIntroduction.m | 177 ----- +kwave/+docfiles/+general/classDocsExample.m | 22 +- +kwave/+docfiles/+general/codeReference.m | 17 + +kwave/+docfiles/+general/howTo.m | 17 + +kwave/+docfiles/+general/kWave.m | 16 + +kwave/+legacy/acousticFieldPropagator.m | 2 +- +kwave/+legacy/angularSpectrum.m | 2 +- +kwave/+legacy/filterTimeSeries.m | 2 +- +kwave/+legacy/focusedAnnulusONeil.m | 2 +- +kwave/+legacy/getAlphaFilter.m | 2 +- +kwave/+legacy/getFDMatrix.m | 2 +- +kwave/+legacy/getkWavePath.m | 2 +- +kwave/+legacy/h5compare.m | 2 +- +kwave/+legacy/kWaveArray.m | 4 +- +kwave/+legacy/kWaveDiffusion.m | 6 +- +kwave/+legacy/kWaveGrid.m | 6 +- +kwave/+legacy/kWaveTransducer.m | 6 +- +kwave/+legacy/kspaceFirstOrder1D.m | 2 +- +kwave/+legacy/kspaceFirstOrder2D.m | 2 +- +kwave/+legacy/kspaceFirstOrder3D.m | 4 +- +kwave/+legacy/kspaceFirstOrderAS.m | 2 +- +kwave/+legacy/kspaceLineRecon.m | 2 +- +kwave/+legacy/kspacePlaneRecon.m | 2 +- +kwave/+legacy/kspaceSecondOrder.m | 2 +- +kwave/+legacy/makeCartArc.m | 2 +- +kwave/+legacy/makeSphere.m | 2 +- +kwave/+legacy/offGridPoints.m | 2 +- +kwave/+legacy/private/checkFieldNames.m | 2 +- +kwave/+legacy/private/enforceFields.m | 4 +- +kwave/+legacy/private/gridDataFast2D.m | 2 +- .../+legacy/private/gridDataFast2D_tsearch.m | 2 +- +kwave/+legacy/private/gridDataFast3D.m | 2 +- .../kspaceFirstOrder_checkOptionalInputs.m | 2 +- ...paceFirstOrder_initialiseMovieParameters.m | 2 +- .../private/kspaceFirstOrder_inputChecking.m | 6 +- .../private/kspaceFirstOrder_saveToDisk.m | 6 +- .../kspaceFirstOrder_scaleSourceTerms.m | 28 +- .../kspaceFirstOrder_setSoundSpeedRef.m | 2 +- +kwave/+legacy/private/tolStar.m | 2 +- +kwave/+legacy/pstdElastic2D.m | 10 +- +kwave/+legacy/pstdElastic3D.m | 8 +- +kwave/+legacy/sharpness.m | 2 +- +kwave/+legacy/voxelPlot.m | 2 +- +kwave/+legacy/writeAttributes.m | 2 +- +kwave/+tests/+linting/TestCodeQuality.m | 22 +- +kwave/+tests/+unit/AbstractTestGrid.m | 24 +- +kwave/+tests/+unit/AbstractTestGridInput.m | 32 +- +kwave/+tests/+unit/TestAcousticMedium.m | 23 +- +kwave/+tests/+unit/TestAcousticSensor.m | 166 ++++ +kwave/+tests/+unit/TestAcousticSolver.m | 709 +++++++++++------- +kwave/+tests/+unit/TestAcousticSolverGrid.m | 25 +- +kwave/+tests/+unit/TestAcousticSource.m | 18 +- +kwave/+tests/+unit/TestComplexSource.m | 28 +- +kwave/+tests/+unit/TestElasticMedium.m | 16 + +kwave/+tests/+unit/TestElasticSource.m | 72 ++ .../+tests/+unit/TestElectromagneticMedium.m | 21 - .../+tests/+unit/TestElectromagneticSource.m | 38 - +kwave/+tests/+unit/TestExpandMatrix.m | 26 +- +kwave/+tests/+unit/TestFourierCollocation.m | 96 ++- +kwave/+tests/+unit/TestGrid.m | 22 +- +kwave/+tests/+unit/TestLogger.m | 44 +- +kwave/+tests/+unit/TestMaterials.m | 204 +++++ +kwave/+tests/+unit/TestMedium.m | 252 ++++++- +kwave/+tests/+unit/TestSplitFieldPML.m | 20 +- +kwave/+tests/+unit/TestThermalMedium.m | 16 + +kwave/+tests/+unit/TestThermalSolver.m | 17 + +kwave/+tests/+unit/TestThermalSolverGrid.m | 40 +- +kwave/+tests/+unit/TestThermalSource.m | 20 +- +kwave/+tests/TestType.m | 22 +- +kwave/+tests/runTests.m | 18 +- .../+toolbox/@AcousticMedium/AcousticMedium.m | 23 +- .../+toolbox/@AcousticSensor/AcousticSensor.m | 179 +++++ .../+toolbox/@AcousticSolver/AcousticSolver.m | 65 +- .../@AcousticSolver/autoComputeTimeStep.m | 12 +- .../@AcousticSolver/executeTimeStep.m | 129 +++- .../@AcousticSolver/kappaSplitCorrection.m | 108 +-- .../setAbsorptionCoefficients.m | 37 +- .../@AcousticSolver/setInitialConditions.m | 8 +- .../@AcousticSolver/setkSpaceCorrection.m | 114 +-- .../+toolbox/@AcousticSource/AcousticSource.m | 37 +- .../+toolbox/@ComplexSource/ComplexSource.m | 20 +- .../+toolbox/@ElasticMedium/ElasticMedium.m | 16 + .../+toolbox/@ElasticSource/ElasticSource.m | 52 ++ .../ElectromagneticMedium.m | 47 -- .../ElectromagneticSource.m | 60 -- .../@FourierCollocation/FourierCollocation.m | 33 +- +kwave/+toolbox/@FourierCollocation/curl.m | 19 +- .../+toolbox/@FourierCollocation/divergence.m | 17 +- .../@FourierCollocation/divergenceSplit.m | 17 +- .../@FourierCollocation/fracLaplacian.m | 118 ++- .../+toolbox/@FourierCollocation/gradient.m | 19 +- .../+toolbox/@FourierCollocation/plotField.m | 18 +- +kwave/+toolbox/@FourierCollocation/sinc.m | 11 +- +kwave/+toolbox/@FourierCollocation/stagger.m | 23 +- +kwave/+toolbox/@Grid/Grid.m | 65 +- +kwave/+toolbox/@Grid/assignWithGridPadding.m | 18 +- +kwave/+toolbox/@Grid/displayGridSize.m | 22 +- +kwave/+toolbox/@Grid/getWavenumbers.m | 22 +- +kwave/+toolbox/@Grid/highestPrimeFactors.m | 21 +- .../+toolbox/@Grid/returnWithoutGridPadding.m | 18 +- +kwave/+toolbox/@Grid/validateSize.m | 22 +- +kwave/+toolbox/@GridField/GridField.m | 20 +- .../+toolbox/@GridField/createGridFieldsMap.m | 20 +- .../+toolbox/@GridFieldType/GridFieldType.m | 16 + +kwave/+toolbox/@GridInput/GridInput.m | 29 +- .../@GridInput/checkRequiredProperties.m | 18 +- .../+toolbox/@GridInput/getPropertyGroups.m | 20 +- +kwave/+toolbox/@GridInput/subsasgn.m | 22 +- +kwave/+toolbox/@GridInput/subsref.m | 20 +- +kwave/+toolbox/@LogLevels/LogLevels.m | 16 + +kwave/+toolbox/@Logger/Logger.m | 48 +- +kwave/+toolbox/@Materials/Materials.m | 297 ++++++++ +kwave/+toolbox/@Medium/BonA.m | 50 -- +kwave/+toolbox/@Medium/BonAPadded.m | 43 -- +kwave/+toolbox/@Medium/Medium.m | 347 +++++++-- +kwave/+toolbox/@Medium/absorptionCoeff.m | 50 -- .../+toolbox/@Medium/absorptionCoeffPadded.m | 44 -- +kwave/+toolbox/@Medium/absorptionPower.m | 61 -- +kwave/+toolbox/@Medium/density.m | 50 -- +kwave/+toolbox/@Medium/densityPadded.m | 46 -- +kwave/+toolbox/@Medium/soundSpeed.m | 50 -- +kwave/+toolbox/@Medium/soundSpeedPadded.m | 44 -- +kwave/+toolbox/@Medium/specificHeat.m | 50 -- +kwave/+toolbox/@Medium/specificHeatPadded.m | 44 -- +kwave/+toolbox/@Medium/thermalConductivity.m | 50 -- .../@Medium/thermalConductivityPadded.m | 43 -- +kwave/+toolbox/@Sensor/Sensor.m | 84 +++ +kwave/+toolbox/@Settings/Settings.m | 11 +- +kwave/+toolbox/@Solver/Solver.m | 37 +- .../+toolbox/@SplitFieldPML/SplitFieldPML.m | 6 +- +kwave/+toolbox/@SplitFieldPML/applyPML.m | 6 +- .../@SplitFieldPML/getQuarticPMLProfile.m | 10 +- .../+toolbox/@SplitFieldPML/setupQuarticPML.m | 10 +- .../+toolbox/@ThermalMedium/ThermalMedium.m | 16 + .../+toolbox/@ThermalSolver/ThermalSolver.m | 24 +- .../@ThermalSolver/autoComputeTimeStep.m | 6 +- .../+toolbox/@ThermalSolver/executeTimeStep.m | 11 +- .../@ThermalSolver/setInitialConditions.m | 9 +- .../+toolbox/@ThermalSource/ThermalSource.m | 16 + .../@TimeDomainSolver/TimeDomainSolver.m | 31 +- +kwave/+toolbox/@TimeDomainSolver/run.m | 73 +- +kwave/+toolbox/BuildMaterialTable.m | 54 -- +kwave/+toolbox/Toolbox_Functions.m | 38 + +kwave/+toolbox/expandMatrix.m | 54 +- .../AcosuticSolverExamples.m | 291 ------- .../+initialvalueproblems/Tutorials.m | 21 + .../homogeneousMedium2D.m | 42 -- .../t01_AcousticSolverExamples1D.m | 179 +++++ .../t02_MoreAcousticSolverExamples1D.m | 245 ++++++ .../t03_AcousticSolverExamples2D.m | 266 +++++++ .../t04_MoreAcousticSolverExamples2D.m | 295 ++++++++ +kwave/+utilities/GenerateDocumentation.m | 283 ------- +kwave/+utilities/Utility_Functions.m | 24 + +kwave/+utilities/formatDuration.m | 36 +- +kwave/+utilities/getkWavePath.m | 18 +- +kwave/+utilities/mustBeAllOrNoneEmpty.m | 10 +- +kwave/+utilities/mustBeEmptyOrComplex.m | 20 +- +kwave/+utilities/mustBeEqualSize.m | 18 +- +kwave/+utilities/parseTitle.m | 29 - +kwave/+utilities/plotFieldsDiff.m | 18 +- +kwave/+utilities/scaleSI.m | 42 +- .all-contributorsrc | 150 ++++ .codespellrc | 2 + .github/ISSUE_TEMPLATE/bug-form.yml | 58 ++ .github/ISSUE_TEMPLATE/bug_template.md | 25 - .github/ISSUE_TEMPLATE/config.yml | 1 + .../ISSUE_TEMPLATE/feature_request-form.yml | 37 + .github/ISSUE_TEMPLATE/issue_template.md | 13 - .../pull_request_template.md | 23 - .github/pull_request_template.md | 31 + .github/workflows/code_quality.yml | 6 +- .github/workflows/contributors.yml | 89 +++ .github/workflows/docs.yml | 82 +- .github/workflows/lint-license.yml | 36 + .github/workflows/pre-commit.yml | 18 + .github/workflows/tests.yml | 8 +- .gitignore | 22 +- .pre-commit-config.yaml | 31 + README.md | 39 - docs/CITATION.cff | 48 ++ docs/CONTRIBUTING.md | 107 +++ LICENSE.txt => docs/LICENSE.md | 332 ++++---- docs/README.md | 97 +++ docs/apply_license_mlab_check.py | 145 ++++ docs/codeOfConduct.md | 52 ++ docs/codingStandard.md | 130 ++++ docs/developerIntroduction.md | 113 +++ docs/git-model.png | Bin 0 -> 124925 bytes docs/maintainerDocs.md | 51 ++ docs/test_doc_generation.ps1 | 18 + docs/test_doc_generation.py | 18 + docs/test_doc_generation.sh | 20 + info.xml | 4 +- mkdocs.yml | 34 + requirements.txt | 4 + 199 files changed, 6879 insertions(+), 3195 deletions(-) create mode 100644 +kwave/+devtools/GenerateDocumentation.m rename +kwave/{+utilities => +devtools}/gitChangedFiles.m (56%) create mode 100644 +kwave/+devtools/parseTitle.m delete mode 100644 +kwave/+docfiles/+developer/codingStandard.m delete mode 100644 +kwave/+docfiles/+developer/developerIntroduction.m create mode 100644 +kwave/+docfiles/+general/codeReference.m create mode 100644 +kwave/+docfiles/+general/howTo.m create mode 100644 +kwave/+tests/+unit/TestAcousticSensor.m create mode 100644 +kwave/+tests/+unit/TestElasticSource.m delete mode 100644 +kwave/+tests/+unit/TestElectromagneticMedium.m delete mode 100644 +kwave/+tests/+unit/TestElectromagneticSource.m create mode 100644 +kwave/+tests/+unit/TestMaterials.m create mode 100644 +kwave/+toolbox/@AcousticSensor/AcousticSensor.m create mode 100644 +kwave/+toolbox/@ElasticSource/ElasticSource.m delete mode 100644 +kwave/+toolbox/@ElectromagneticMedium/ElectromagneticMedium.m delete mode 100644 +kwave/+toolbox/@ElectromagneticSource/ElectromagneticSource.m create mode 100644 +kwave/+toolbox/@Materials/Materials.m delete mode 100644 +kwave/+toolbox/@Medium/BonA.m delete mode 100644 +kwave/+toolbox/@Medium/BonAPadded.m delete mode 100644 +kwave/+toolbox/@Medium/absorptionCoeff.m delete mode 100644 +kwave/+toolbox/@Medium/absorptionCoeffPadded.m delete mode 100644 +kwave/+toolbox/@Medium/absorptionPower.m delete mode 100644 +kwave/+toolbox/@Medium/density.m delete mode 100644 +kwave/+toolbox/@Medium/densityPadded.m delete mode 100644 +kwave/+toolbox/@Medium/soundSpeed.m delete mode 100644 +kwave/+toolbox/@Medium/soundSpeedPadded.m delete mode 100644 +kwave/+toolbox/@Medium/specificHeat.m delete mode 100644 +kwave/+toolbox/@Medium/specificHeatPadded.m delete mode 100644 +kwave/+toolbox/@Medium/thermalConductivity.m delete mode 100644 +kwave/+toolbox/@Medium/thermalConductivityPadded.m create mode 100644 +kwave/+toolbox/@Sensor/Sensor.m delete mode 100644 +kwave/+toolbox/BuildMaterialTable.m create mode 100644 +kwave/+toolbox/Toolbox_Functions.m delete mode 100644 +kwave/+tutorials/+initialvalueproblems/AcosuticSolverExamples.m create mode 100644 +kwave/+tutorials/+initialvalueproblems/Tutorials.m delete mode 100644 +kwave/+tutorials/+initialvalueproblems/homogeneousMedium2D.m create mode 100644 +kwave/+tutorials/+initialvalueproblems/t01_AcousticSolverExamples1D.m create mode 100644 +kwave/+tutorials/+initialvalueproblems/t02_MoreAcousticSolverExamples1D.m create mode 100644 +kwave/+tutorials/+initialvalueproblems/t03_AcousticSolverExamples2D.m create mode 100644 +kwave/+tutorials/+initialvalueproblems/t04_MoreAcousticSolverExamples2D.m delete mode 100644 +kwave/+utilities/GenerateDocumentation.m create mode 100644 +kwave/+utilities/Utility_Functions.m delete mode 100644 +kwave/+utilities/parseTitle.m create mode 100644 .all-contributorsrc create mode 100644 .codespellrc create mode 100644 .github/ISSUE_TEMPLATE/bug-form.yml delete mode 100644 .github/ISSUE_TEMPLATE/bug_template.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request-form.yml delete mode 100644 .github/ISSUE_TEMPLATE/issue_template.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE/pull_request_template.md create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/contributors.yml create mode 100644 .github/workflows/lint-license.yml create mode 100644 .github/workflows/pre-commit.yml create mode 100644 .pre-commit-config.yaml delete mode 100644 README.md create mode 100644 docs/CITATION.cff create mode 100644 docs/CONTRIBUTING.md rename LICENSE.txt => docs/LICENSE.md (97%) create mode 100644 docs/README.md create mode 100644 docs/apply_license_mlab_check.py create mode 100644 docs/codeOfConduct.md create mode 100644 docs/codingStandard.md create mode 100644 docs/developerIntroduction.md create mode 100644 docs/git-model.png create mode 100644 docs/maintainerDocs.md create mode 100644 docs/test_doc_generation.ps1 create mode 100644 docs/test_doc_generation.py create mode 100644 docs/test_doc_generation.sh create mode 100644 mkdocs.yml create mode 100644 requirements.txt diff --git a/+kwave/+devtools/GenerateDocumentation.m b/+kwave/+devtools/GenerateDocumentation.m new file mode 100644 index 00000000..b3e4d661 --- /dev/null +++ b/+kwave/+devtools/GenerateDocumentation.m @@ -0,0 +1,423 @@ +%% GenerateDocumentation +% *Package:* kwave.devtools +% +% Generate help documentation. +% +%% Syntax +% kwave.devtools.GenerateDocumentation +% +%% Description +% |GenerateDocumentation| is a static class that is called to generate the +% help files that appear in the MATLAB help browser and the documentation website. +% The documentation is generated in several stages: +% +% # The |.m| files in specified directories are converted directly to |.html| +% using the MATLAB +% function and to |.md| using the +% function. Text should be written using MATLAB publishing markup, which is parsed as headings, +% code, etc. +% # The generated |.html| and |.md| files are modified to add links between +% related functions and classes. +% # A |helptoc.xml| file is automatically created, adding links to the +% generated |.html| files. Similarly, the appropriate |SUMMARY.md| files +% are generated in the appropriate file tree structure that contains the +% markdown files (ready to be turned into GitHub pages with ) +% # The documentation search database is created using +% . +% +% This class is provided for generating documentation during development, +% and for preparing the documentation published on GitHub pages and packaged with a release. If you have +% downloaded a packaged release, there should be no need to call this +% class. However, if you have directly cloned the repository and are +% working with bleeding-edge changes, you can call |GenerateDocumentation| +% to (re-)generate the help files. + +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + +classdef GenerateDocumentation + + properties(SetAccess=immutable, Hidden=true) + helpDirHtml; + helpDirMd; + rootPath; + end + + % Constructor. + methods(Static) + function obj = GenerateDocumentation() + + % Setup paths. The toolbox root directory is two levels above + % +kwave/+utilities. Add back forward slash to directory if on + % linux or mac. + [mFilePath, ~, ~] = fileparts(mfilename('fullpath')); + mFilePathParts = split(mFilePath, filesep); + obj.rootPath = fullfile(mFilePathParts{1:end-2}); + if isunix + obj.rootPath = [filesep obj.rootPath]; + end + addpath(obj.rootPath); + obj.helpDirHtml = fullfile(obj.rootPath, 'docs/helpfiles'); + obj.helpDirMd = fullfile(obj.rootPath, 'docs/helpfilesweb'); + obj.createHelpDir; + + % Generate HTML and md documentation files. + generalDocsFilenames = obj.generateDocFiles('/+kwave/+docfiles/+general', '.'); + tutorialsFilenames = obj.generateDocFiles('/+kwave/+tutorials/+initialvalueproblems', 'Tutorials', evalCode=true, showCode=false); + toolboxFilenames = obj.generateDocFiles('/+kwave/+toolbox', 'Toolbox_Functions'); + testFilenames = obj.generateDocFiles('/+kwave/+tests', 'Test_Functions'); + utilityFilenames = obj.generateDocFiles('/+kwave/+utilities', 'Utility_Functions'); + devtoolsFilenames = obj.generateDocFiles('/+kwave/+devtools', 'Developer_Tools'); + + % Build helptoc. + obj.helpTocStart; + obj.addToXML(['' 'How To']); + obj.helpTocAddSection(tutorialsFilenames, 'Tutorials'); + obj.addToXML(''); + obj.addToXML(['' 'Code Reference']); + obj.helpTocAddSection(toolboxFilenames, 'Toolbox Functions'); + obj.helpTocAddSection(utilityFilenames, 'Utility Functions'); + obj.addToXML(''); + obj.helpTocFinish; + + % Build SUMMARY.md for each subfolder/subsection + % We don't need a SUMMARY.md for the generalDocs, as it's only + % the Class Example file and we don't want it in a subheading. + obj.tocMd(tutorialsFilenames, 'Tutorials'); + obj.tocMd(toolboxFilenames, 'Toolbox_Functions'); + obj.tocMd(testFilenames, 'Test_Functions', excludeClassMethods=false); + obj.tocMd(utilityFilenames, 'Utility_Functions'); + obj.tocMd(devtoolsFilenames, 'Developer_Tools'); + + % Build searchable docs. + disp('Generating search database...'); + builddocsearchdb(obj.helpDirHtml); + + end + end + + methods(Access=private, Hidden=true) + + % Create empty helpfiles directory. + function createHelpDir(obj) + if exist(obj.helpDirHtml, 'file') + rmdir(obj.helpDirHtml, 's'); + end + mkdir(obj.helpDirHtml); + if exist(obj.helpDirMd, 'file') + rmdir(obj.helpDirMd, 's'); + end + mkdir(obj.helpDirMd); + end + + % Convert m-files in specified directory to HTML using publish and to md using export. + function mFilenames = generateDocFiles(obj, relativeFolder, mdSubFolder, options) + arguments + obj + relativeFolder + mdSubFolder + options.showCode = false; + options.evalCode = false; + end + + % Get namespace from folder + nameSpace = obj.folderToNamespace(relativeFolder); + + % Find all m-files contained in relativeFolder and + % sub-directories. + disp(['Generating HTML for ' relativeFolder '...']); + absolutePath = fullfile(obj.rootPath, relativeFolder); + mFilenames = dir(fullfile(absolutePath, '**/*.m')); + numFiles = length(mFilenames); + + % Initialise additional properties that we will set. + [mFilenames(:).isClassMethod] = deal(false); + [mFilenames(:).isClass] = deal(false); + [mFilenames(:).className] = deal(''); + [mFilenames(:).title] = deal(''); + [mFilenames(:).htmlFileName] = deal(''); + [mFilenames(:).mdFileName] = deal(''); + + % Loop over m-files. + for ind = 1:numFiles + + % Get filename without extension. + [~, filename, ~] = fileparts(mFilenames(ind).name); + + % Get relative folder (may be empty if in root folder). + mFileRelativeFolder = erase(mFilenames(ind).folder, absolutePath); + mFileRelativeFolder = erase(mFileRelativeFolder, filesep); + + % We need to treat class methods that are in separate files + % slightly differently, as these can only be compiled if we + % change directories. To do this, check if the m-file is in + % a class folder AND has a different name to the class. + % Otherwise, we need to prepend the namespace to the + % filename. If there is a nested namespace, this also needs + % prepending. + if obj.isClass(mFileRelativeFolder, filename) + mFilenames(ind).isClass = true; + end + + bareFilename = filename; + if obj.isClassMethod(mFileRelativeFolder, filename) + mFilenames(ind).isClassMethod = true; + mFilenames(ind).className = [mFileRelativeFolder(2:end) '.m']; % Convert folder name to class name by removing the leading "@" character. + cd(mFilenames(ind).folder); + filename = [filename '.m']; %#ok + else + % Not a class or class method + filename = [nameSpace filename]; %#ok + end + + % Print details of conversions. + disp(['Converting ', filename, ' to HTML (', int2str(ind), '/', int2str(numFiles), ')']); + + % Extract title used for entry in helptoc.xml from first + % line of code documentation. + mFilenames(ind).title = kwave.devtools.parseTitle(fullfile(mFilenames(ind).folder, mFilenames(ind).name)); + + % Publish to html. + htmlFile = publish(filename, ... + 'format', 'html', ... + 'outputDir', obj.helpDirHtml, ... + 'evalCode', options.evalCode, ... + 'showCode', options.showCode); + + % Replace deprecated tags with tags + htmlFile_content = fileread(htmlFile); + + htmlFile_content = strrep(htmlFile_content, '', ''); + htmlFile_content = strrep(htmlFile_content, '', ''); + + htmlFile_handle = fopen(htmlFile, 'w'); + fwrite(htmlFile_handle, htmlFile_content); + fclose(htmlFile_handle); + + % Publish to md. + inputFunctionFullFileName = fullfile(mFilenames(ind).folder, mFilenames(ind).name); + outputFullFolderName = fullfile(obj.helpDirMd,mdSubFolder); + mkdir(outputFullFolderName); + % Create full paths for .mlx (matlab live script) and .md (markdown) files. + fullFileNameMLX = fullfile(outputFullFolderName, [bareFilename '.mlx']); + fullFileNameMD = fullfile(outputFullFolderName, [bareFilename '.md']); + % Print details of conversion from .m to .md + disp(['Converting ', filename, ' to ' [bareFilename '.md']]); + % Converts the .m file into a .mlx file and saves it. + matlab.internal.liveeditor.openAndSave(inputFunctionFullFileName, fullFileNameMLX); + % Exports the .mlx file into a .md file. + export(fullFileNameMLX, fullFileNameMD, Format="markdown", Run=options.evalCode, HideCode=~options.showCode); + % Deletes the intermediate .mlx file + delete(fullFileNameMLX); + + + % Rename to include classname if a class method. + [~, filename, ext] = fileparts(htmlFile); + %disp([htmlFile, ' ', fullFileNameMD, ' ', bareFilename]); % full name, full name, bare name + if mFilenames(ind).isClassMethod + [~, className, ~] = fileparts(mFilenames(ind).className); + filename = [className '-' filename]; + newHtmlFile = fullfile(obj.helpDirHtml, [filename '.html']); + movefile(htmlFile, newHtmlFile); + newMdFile = fullfile(outputFullFolderName, [filename '.md']); + movefile(fullFileNameMD, newMdFile); + end + mFilenames(ind).htmlFileName = [filename, ext]; + mFilenames(ind).mdFileName = [filename, '.md']; + + % Change back to root directory. + cd(obj.rootPath); + + end + + % Add relative links to class methods from class documentation. + for ind1 = 1:numFiles + for ind2 = 1:numFiles + % Add links to the contents of the toc headers while we + % are here. Those toc html files have the same name as + % the mdSubFolder of the documentation section + % (strcmp(bareFilename, mdSubFolder)). We do not want to + % link in the toc the class method files (~mFilenames(ind2).isClassMethod), + % or the toc file itself + % (~strcmp(mFilenames(ind2).htmlFileName, [mdSubFolder '.html']) + [~, bareFilename, ~] = fileparts(mFilenames(ind1).name); + if strcmp(bareFilename, mdSubFolder) && ~mFilenames(ind2).isClassMethod && ~strcmp(mFilenames(ind2).htmlFileName, [mdSubFolder '.html']) + obj.fixLinks(mFilenames(ind1), mFilenames(ind2), outputFullFolderName); + end + if mFilenames(ind1).isClass && mFilenames(ind2).isClassMethod && strcmp(mFilenames(ind2).className, mFilenames(ind1).name) + obj.fixLinks(mFilenames(ind1), mFilenames(ind2), outputFullFolderName); + end + end + end + end + + % Given a source filename object "fromObj" that contains references + % to other files, and a filename destination object "toObj" that + % contains the file that is referred to, add a relative link to the + % destination file from the source file in both html and md. + % This function is used to fix links in documentation between + % classes and their methods, among other things. + % The md output folder path has to be given as well, because it + % contains a subfolder. + function fixLinks(obj, fromObj, toObj, outputFullFolderName) + + disp(['Replacing html links to file ', toObj.name, ' from file ', fromObj.name]); + [~, methodName, ~] = fileparts(toObj.name); + htmlFileName = fromObj.htmlFileName; + htmlFile = fullfile(obj.helpDirHtml, htmlFileName); + + % Read in HTML file. + fid = fopen(htmlFile, 'r'); + fileContents = fread(fid, '*char'); + fclose(fid); + + % Replace links, and save to HTML file replacing + % contents. The |methodName| syntax is published as + % methodName. The html flags are included + % in the search to avoid adding links to code + % snippets. + fileContents = strrep(fileContents.', ... + ['' methodName ''], ... + ['' obj.generateLink(toObj.htmlFileName, methodName) '']); + fid = fopen(htmlFile, 'w'); + fprintf(fid, '%s', fileContents.'); + fclose(fid); + + disp(['Replacing md links to file ', toObj.name, ' from file ', fromObj.name]); + mdFileName = fromObj.mdFileName; + mdFile = fullfile(outputFullFolderName, mdFileName); + + % Read in md file. + fid = fopen(mdFile, 'r'); + fileContents = fread(fid, '*char'); + fclose(fid); + + % Add links, and save to md file. The |methodName| + % syntax is published as `methodName`. The quotes are included + % in the search to avoid adding links to code snippets. + fileContents = strrep(fileContents.', ... + ['`' methodName '`'], ... + ['[' methodName '](' toObj.mdFileName ')']); + fid = fopen(mdFile, 'w'); + fprintf(fid, '%s', fileContents.'); + fclose(fid); + + end + + % Given a directory (e.g. '/+kwave/+utils'), convert to the + % corresponding namespace (e.g. 'kwave.utils') + function nameSpace = folderToNamespace(~, folder) + + nameSpace = replace(folder, '/+', '.'); + + % Strip any leading . + if strcmp(nameSpace(1), '.') + nameSpace = nameSpace(2:end); + end + + % Add a trailing . + if ~isempty(nameSpace) + nameSpace = [nameSpace, '.']; + end + + end + + % Given a folder (e.g. '@Grid') and a filename (e.g. + % 'Grid.m'), return true if the file represents the class + % itself (as opposed to a class method) + function isClass = isClass(~, mFileRelativeFolder, filename) + isClass = ~isempty(mFileRelativeFolder) && strcmp(extractAfter(mFileRelativeFolder, 1), filename); + end + + % Given a folder (e.g. '@Grid') and a filename (e.g. + % 'validateSize.m'), return true if the file represents a class + % method + function isClassMethod = isClassMethod(obj, mFileRelativeFolder, filename) + isClassMethod = ~isempty(mFileRelativeFolder) && ~obj.isClass(mFileRelativeFolder, filename); + end + + % Start generation of helptoc.xml. + function helpTocStart(obj) + disp('Generating helptoc.xml...'); + obj.addToXML(''); + obj.addToXML(''); + obj.addToXML('k-Wave II'); + end + + % Add links to html for all functions, excluding class methods. + function helpTocAddSection(obj, mFilenames, heading, options) + arguments + obj + mFilenames + heading + options.addHeader = true; + end + + if options.addHeader + obj.addToXML(['' heading]); + end + for ind = 1:length(mFilenames) + if ~mFilenames(ind).isClassMethod && ~strcmp(mFilenames(ind).htmlFileName, [strrep(heading,' ','_') '.html']) + obj.addToXML(['' mFilenames(ind).title '']); + end + end + if options.addHeader + obj.addToXML(''); + end + end + + % Finish generation of helptoc.xml. + function helpTocFinish(obj) + obj.addToXML(''); + obj.addToXML(''); + end + + % Convenience function to call writelines. + function addToXML(obj, line) + filename = fullfile(obj.helpDirHtml, 'helptoc.xml'); + writelines(line, filename, 'WriteMode','append'); + end + + % Create SUMMARY.md toc for the markdown versions of the files, + % excluding class methods by default. + function tocMd(obj, mFilenames, mdSubFolder, options) + arguments + obj + mFilenames + mdSubFolder + options.excludeClassMethods = true; + end + outputFullFolderName = fullfile(obj.helpDirMd,mdSubFolder); + filename = fullfile(outputFullFolderName, 'SUMMARY.md'); + for ind = 1:length(mFilenames) + % Exclude the header toc files from SUMMARY.md as well, + % those are only needed for the html (~strcmp(mFilenames(ind).mdFileName, [strrep(mdSubFolder,' ','_') '.md']) + if ((options.excludeClassMethods && ~mFilenames(ind).isClassMethod) || ~options.excludeClassMethods) && ~strcmp(mFilenames(ind).mdFileName, [strrep(mdSubFolder,' ','_') '.md']) + writelines(['* [' mFilenames(ind).title '](' mFilenames(ind).mdFileName ')'], filename, 'WriteMode','append'); + end + end + end + + % Convenience function to generate HTML link to file. + % + % htmlFilename should include the .html file extension + function link = generateLink(~, htmlFilename, methodName) + link = ['' methodName '']; + end + + end + +end diff --git a/+kwave/+utilities/gitChangedFiles.m b/+kwave/+devtools/gitChangedFiles.m similarity index 56% rename from +kwave/+utilities/gitChangedFiles.m rename to +kwave/+devtools/gitChangedFiles.m index 6f3155b4..dde504f9 100644 --- a/+kwave/+utilities/gitChangedFiles.m +++ b/+kwave/+devtools/gitChangedFiles.m @@ -1,5 +1,5 @@ -%% gitChangedFiles -% *Package:* kwave.utilities +%% git Changed Files +% *Package:* kwave.devtools % % Return a list of files changed on current git branch. % @@ -10,16 +10,32 @@ %% Description % Returns a cell array of file names that have been modified between the % current git branch and main. By default, file names are given with -% paths relative to the k-Wave root directoy. To return absolute file +% paths relative to the k-Wave root directory. To return absolute file % paths, call |gitChangedFiles(true)|. % %% Input Arguments % * |returnAbsolutePath| - (logical) Return absolute (true) or relative -% (false) path for modified files. Default = false. +% (false) path for modified files. Default = false. % %% Output Arguments % * |fileList| - (string) Cell array of modified files. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + function fileList = gitChangedFiles(returnAbsolutePath) arguments diff --git a/+kwave/+devtools/parseTitle.m b/+kwave/+devtools/parseTitle.m new file mode 100644 index 00000000..3f18d600 --- /dev/null +++ b/+kwave/+devtools/parseTitle.m @@ -0,0 +1,45 @@ +%% Parse Title +% *Package:* kwave.devtools +% +% Convenience function to extract the text appearing on the first line of +% an m-file after the characters "%% ". +% +%% Syntax +% titleParsed = parseTitle(filename) +% +%% Description +% |parseTitle| extracts the text appearing on the first line of an m-file +% after the characters "%% ". +% +%% Input Arguments +% * |filename| - (string) Filename to get title from. +% +%% Output Arguments +% * |titleString| - (string) Extracted title. + +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + +function titleString = parseTitle(filename) + fid = fopen(filename); + titleLine = fgetl(fid); + if any(strfind(titleLine, "%% ")) + titleString = erase(titleLine, "%% "); + else + error('GenerateDocumentation:missingTitleComment', '%s is missing a title comment.', filename); + end + fclose(fid); +end diff --git a/+kwave/+docfiles/+developer/codingStandard.m b/+kwave/+docfiles/+developer/codingStandard.m deleted file mode 100644 index 26f5cf7c..00000000 --- a/+kwave/+docfiles/+developer/codingStandard.m +++ /dev/null @@ -1,153 +0,0 @@ -%% Coding Standard -% -% MATLAB coding standard used for k-Wave II. -% -%% Naming -% * Namespaces uses |+lowercase|. -% * Class names use |TitleCase|. -% * Function names and script filenames use |camelCase|. -% * Test classes use |TestMyClassName| or |TestMyFunctionName|. -% * Variables use |camelCase|. -% * Name-Value arguments use |TitleCase|. -% * Descriptive naming should be used for the user-facing API where -% possible, e.g., |soundSpeed| rather than |c|. Break this rule for -% internal variables for commonly used symbols (e.g. |c| for sound speed, -% |p| for pressure), as long as the symbol is well known and stays within -% a limited scope (e.g., a function) where a short comment at the top -% says what the single letter stands for. -% -%% Code -% * Each line of code should only contain one statement. -% * Each line of executed code should be terminated with a semicolon -% without a preceding space. -% * Class properties and argument validation code should not be terminated -% with a semicolon. -% * For element-wise multiply, divide, and power operations, use a -% preceding period, even when acting on a scalar. -% * Always use a period before the transpose operator, unless the conjugate -% transpose is required. -% * Mathematical operands and assignments, e.g., |+ * && = >| etc., should -% be space padded. Exceptions are power |^|, the colon operator |:|, the -% not operator |~|, and the |-| sign when used to signify a negative -% number. - -x = 1:10; -x = a .* b.^2; -x = a && ~b; -x = 10 - 3.4; -x = -1e-3; -x = [1, 2, 3].'; - -%% -% * Break the previous two rules if it helps readability for simple -% multiply and divide operations using scalar literals. - -x = 2*pi; -x = 1/pi; - -%% -% * Decimals should have a preceding zero. -% * Always preceed the imaginary unit with a number, e.g., |1i|. - -x = 0.5 + 1i; - -%% -% * Align multiple similar lines of code if it makes sense for readability -% (similarly for comments). - -windSpeed = exp( x - w .* t); -waterTemperature = exp(-x - 3*w .* t); - -%% -% * If long lines of code become difficult to read, then break lines of -% code using |...| and align for readability. - -%% Brackets and Commas -% * Brackets should have no interior spaces. Break this rule if needed for -% readability when using multiple nested brackets. -% * Brackets should follow immediately after function names with no space. -% * Commas should be used to separate multiple inputs, outputs, or matrix -% values. -% * All commas should be followed by a space. - -x = (a - b).^2; -[a, b, c] = myFunc(x, y, z); -x = [1, 2, 3]; -x = [1; 2; 3]; - -%% -% * Logical statements should be grouped using brackets - -if (myHeight > 10) && (myDog.Name == "Fido") - disp('Excellent.'); -end - -%% Code Comments -% * All code comments should be written as sentences, including capitals -% and periods. -% * Comments should be limited to a maximum column width of 75 characters -% (the default in MATLAB). -% * Comments that span multiple lines should be aligned at the first -% character of each line. -% * Code comments should appear on the previous line, rather -% than inline. Break this rule with common sense. -% * Comments should be preceded by a blank line. -% -%% Indentation and Line Breaks -% * Statements inside conditional loops and if-statements (etc) should be -% indented. -% * Each code indent level should be 4 spaces. -% * Include a blank line at the beginning and end of the conditional IFF -% the code on the inside contains a comment. -% * All files should end with a blank new line. -% * Trailing spaces after lines of code or comments should be removed. - -% Increment the counter if radius exceed maximum value. There are no -% comments inside the if statement, so do not include additional line -% breaks. -if radius > maximumRadius - counter = counter + 1; -end - -% Loop through possible heights and calculate weight. There are comments -% inside the if state, so include additional line breaks. -for index = 1:length(heightArray) - - % Assign current height. - height = heightArray(index); - - % Calculate current weight based on time of day. - weight(index) = height .* now; - -end - -%% Error messages -% * Error messages should use the |kwave.toolbox.Logger| class. -% * Error messages should begin with a capital letter (except when using a -% variable or function name) and end with a period. -% * Error messages should use a two part identifier in the form -% |ClassName:errorIdentifier| or -% |functionName:errorIdentifier|. - -kwave.toolbox.Logger.error('Grid:incorrectInputSize', 'gridSpacing must be a scalar or the same length as gridSize.'); - -%% Class and Function Documentation -% Classes should be written using -% . Class and function documentation should be -% written using -% . The following structure and headings should be used. -% . -% -% * functionName or className -% * Syntax -% * Description -% * Examples -% * Input Arguments -% * Name-Value Arguments -% * Output Arguments -% * Properties (classes only) -% * Methods (classes only) -% * See Also diff --git a/+kwave/+docfiles/+developer/developerIntroduction.m b/+kwave/+docfiles/+developer/developerIntroduction.m deleted file mode 100644 index 3310f586..00000000 --- a/+kwave/+docfiles/+developer/developerIntroduction.m +++ /dev/null @@ -1,177 +0,0 @@ -%% Developer Introduction -% -% Introduction to k-Wave II for developers. -% -%% Overview -% k-Wave II is an open-source MATLAB toolbox used to solve differential -% equations, with a particular focus on wave problems in acoustics. The -% unifying thread for the solvers is that spatial gradients are computed -% using a Fourier collocation spectral method. This has many advantages, -% including spectral convergence for smooth functions, and a known -% analytical form for the band-limited interpolant, which is useful for -% implementing stair-case free sources, for example. -% -% The main components of the toolbox are written using an object-orientated -% programming design approach, primarily using |handle| classes. Sets of -% functionality (such as medium inputs for a particular solver) are grouped -% into classes. Base classes are used for common functionality that needs -% to be re-used multiple times. This adheres to the don't repeat yourself -% (DRY) software development principle. Some additional helper functions -% with limited scope are written as standalone functions. -% -% The software is written with the following four guiding principles, in -% decreasing order of importance: -% -% # *The code is accurate.* The solvers should avoid narrow scope -% assumptions which are not always applicable. The code should also -% implement appropriate input validation and error handling, and be covered -% by appropriate tests (see for -% further details). -% # *The code is easy to use.* Careful thought should be given to the class -% interfaces and default values, and the user-facing API should be as -% stable as possible over time. All code should be well documented, and the -% documentation should include user examples and tutorials. Code should -% also provide sufficient feedback and warnings to guide users. -% # *The code is sustainable.* The development, testing, and release -% workflows should be sufficiently automated that it is straightforward to -% implement new features and bug fixes. All code should follow the -% . -% # *The code is fast.* Code should be profiled regularly, and where -% possible, refactored to improve computational efficiency. -% -%% Repository Structure -% The code is grouped into -% (namespaces). The basic folder structure is as follows: -% -% ├── .github -% │ ├── ISSUE_TEMPLATE (GitHub issue templates) -% │ └── workflows (GitHub actions) -% ├── +kwave -% │ ├── +docfiles (Documentation files) -% │ │ ├── +developer (Developer Documentation) -% │ │ └── +general (Additional Documentation Pages) -% │ ├── +legacy (Copy of k-Wave I) -% │ ├── +tests (Tests) -% │ │ ├── +legacy (Regression tests against k-Wave I) -% │ │ ├── +linting (Linting tests) -% │ │ └── +unit (Unit tests) -% │ ├── +toolbox (Main classes and functions) -% │ ├── +tutorials (Examples and tutorials) -% │ └── +utilities (Developer tools) -% └── helpfiles (Compiled documentation) -% -%% Development Workflow -% The k-Wave development workflow broadly follows -% : -% -% # Anything in the main branch is deployable. -% # To work on something new, create a descriptively named branch off of -% main, starting with the issue number, e.g., |62-implement-pml-class|. -% # Commit to that branch locally and regularly push your work to the same -% named branch on the server. -% # Label commit messages with the issue number, e.g., |commit -m "#62: -% Basic class structure"| -% # When you need feedback or help, or you think the branch is ready for -% merging, open a pull request. -% # After someone else has reviewed and signed off on the feature, you can -% merge it into main. -% -% For git ninjas, |git rebase| should be avoided if multiple people might -% be contributing to a branch (use |git merge| instead). If merging to main -% locally, to maintain the history of the feature branches, |git merge -% --no-ff| (the default if merging via GitHub). -% -%% Writing And Compiling Documentation -% Part of the success of k-Wave can be attributed to the good -% documentation, both of the individual functions and classes, and the -% examples. All code should be documented as outlined in the -% . It can often be easiest to start -% with the . -% -% When adding a new class or function, examples should be added. If the -% code usage is relatively straightforward, examples can be included -% directly in the help documentation for that class or function. For more -% complex classes (e.g., the solver classes), longer tutorials or examples -% should be provided. -% -% # *Tutorials:* These are worked examples stored as |.m| files in the -% |kwave.tutorials| name space. For tutorials, each block of code should be -% surrounded by a discussion guiding the user through the example. The -% discussion should be written using -% . Similar to k-Wave I, concepts introduced in other -% tutorials do not need to be re-introduced. Try and -% focus on a relatively small number of new concepts in each tutorial. The -% tutorial code should generally run fast on basic hardware (< 1 min). -% # *Examples:* These are illustrative examples stored as |.m| files in the -% |kwave.examples| name space. Examples have a wider scope than tutorials, -% and may demonstrate a real-world simulation using realistic grid sizes -% for example (so do not necessarily need to run fast). Examples should -% contain a comprehensive description of what the example does in the -% description of the file, but does not need to have long step-by-step. -% -% The documentation can be automatically compiled by calling -% |kwave.utilities.GenerateDocumentation|. This parses the individual |.m| -% files into |.html| files using . -% Additional documentation files that are not automatically added to the -% table of contents should be stored in |+kwave/+docfiles/+general|. -% -%% Logging And Errors -% Logging messages help users understand code settings and status, and help -% developers debug code. In k-Wave-II, all logging messages (including -% warnings and errors) should be printed using the |kwave.toolbox.Logger| -% class. This provides a consistent interface, allow adjusting the -% verbosity of the output messages, and allows piping the logging messages -% to an external file. -% -%% Toolboxes And External Code -% The core functionality of k-Wave-II should not depend on any MATLAB -% toolboxes. This is to minimise the requirements for non-academic users -% who must purchase a MATLAB license to use k-Wave. Tests can (and do) -% depend on additional toolboxes, as this dependency is fulfilled by the -% GitHub runners (both private and public). -% -% If considering using other external code or libraries (e.g., from the -% file exchange), this should be flagged first on the issue or pull -% request. This way, a discussion can be had about the cost of introducing -% the dependency, and any potential licensing issues. -% -%% Testing Framework -% k-Wave uses the -% . There are several test levels -% (defined in |kwave.tests.TestType|), each of which lives in its own -% namespace: -% -% * *|+unit|:* Unit tests validate individual components of a function or -% class in isolation. -% * *|+linting|:* Linting checks assess code for stylistic and syntactical -% correctness. -% * *|+legacy|:* Legacy tests are regression tests against k-Wave I to -% ensure existing functionality remains unaffected by changes. -% -% Each top level class or function should have at least one corresponding -% unit test. Unit tests should have 100% line coverage. Tests should -% inheret from one of the following: -% -% * |kwave.tests.unit.AbstractTestGridInput| for testing classes that -% derive from |kwave.toolbox.GridInput|. -% * |kwave.tests.unit.AbstractTestGrid| for tests that need to iterate over -% different sized grids. -% * |matlab.unittest.TestCase| for general tests. -% * |matlab.perftest.TestCase| for performance tests. -% -% The filenames for all tests should start with |Test|. Unit tests should -% be named |TestClassName| or |TestFunctionName|. Other tests should be -% given sensible descriptive names. -% -% The linting tests check for code complexity using -% , which is a measure of the decision structure -% complexity of the code. The complexity of all files changed in a pull -% request is automatically added to pull requests as part of the code -% checks action. While a particular number isn't enforced, both developers -% and reviewers should consider whether a re-factoring is appropriate if -% the cylomatic complexity is above 10. diff --git a/+kwave/+docfiles/+general/classDocsExample.m b/+kwave/+docfiles/+general/classDocsExample.m index 95304205..cb39825b 100644 --- a/+kwave/+docfiles/+general/classDocsExample.m +++ b/+kwave/+docfiles/+general/classDocsExample.m @@ -1,6 +1,6 @@ -%% templateClass +%% Template Class % *Package:* kwave.toolbox -% *Superclasses:* kwave.toolbox.GridInput (delete if not applicable) +% *Superclasses:* kwave.toolbox.SuperClass (delete if not applicable) % % Short description of class or function. % @@ -51,4 +51,20 @@ % %% See Also % * |kwave.namespace.relatedkWaveFunction| -% * +% * + +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . diff --git a/+kwave/+docfiles/+general/codeReference.m b/+kwave/+docfiles/+general/codeReference.m new file mode 100644 index 00000000..0503ac26 --- /dev/null +++ b/+kwave/+docfiles/+general/codeReference.m @@ -0,0 +1,17 @@ +%% Code Reference +% Code reference docs +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . diff --git a/+kwave/+docfiles/+general/howTo.m b/+kwave/+docfiles/+general/howTo.m new file mode 100644 index 00000000..6415e127 --- /dev/null +++ b/+kwave/+docfiles/+general/howTo.m @@ -0,0 +1,17 @@ +%% How to +% Guides, tutorials and examples of how to use k-Wave II +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . diff --git a/+kwave/+docfiles/+general/kWave.m b/+kwave/+docfiles/+general/kWave.m index 1579c66c..a67730ae 100644 --- a/+kwave/+docfiles/+general/kWave.m +++ b/+kwave/+docfiles/+general/kWave.m @@ -1,2 +1,18 @@ %% k-Wave II % A MATLAB toolbox for the simulation of acoustic wave fields. + +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . diff --git a/+kwave/+legacy/acousticFieldPropagator.m b/+kwave/+legacy/acousticFieldPropagator.m index fa070cfa..fc2ff1e8 100644 --- a/+kwave/+legacy/acousticFieldPropagator.m +++ b/+kwave/+legacy/acousticFieldPropagator.m @@ -253,7 +253,7 @@ % ========================================================================= % undocumented option to calculate the field pattern using -% kspaceFirstOrderND for comparision +% kspaceFirstOrderND for comparison if use_first_order % create grid diff --git a/+kwave/+legacy/angularSpectrum.m b/+kwave/+legacy/angularSpectrum.m index a863fc29..7b52e5e3 100644 --- a/+kwave/+legacy/angularSpectrum.m +++ b/+kwave/+legacy/angularSpectrum.m @@ -483,7 +483,7 @@ pressure_time_step_exp = cat(3, pressure_time_step, flip(conj(pressure_time_step(:, :, 2:end - 1)), 3)); end - % take inverse Fourier tranform to recover time domain data + % take inverse Fourier transform to recover time domain data pressure_time_step_exp = real(ifft(pressure_time_step_exp, [], 3)); % store maximum pressure diff --git a/+kwave/+legacy/filterTimeSeries.m b/+kwave/+legacy/filterTimeSeries.m index 6fdb1063..963a5722 100644 --- a/+kwave/+legacy/filterTimeSeries.m +++ b/+kwave/+legacy/filterTimeSeries.m @@ -145,7 +145,7 @@ % compute the sampling frequency Fs = 1 / kgrid.dt; -% extract the minium sound speed +% extract the minimum sound speed if isfield(medium, 'sound_speed') % for the fluid code, use medium.sound_speed diff --git a/+kwave/+legacy/focusedAnnulusONeil.m b/+kwave/+legacy/focusedAnnulusONeil.m index 3d7c27c2..9bf904fc 100644 --- a/+kwave/+legacy/focusedAnnulusONeil.m +++ b/+kwave/+legacy/focusedAnnulusONeil.m @@ -77,7 +77,7 @@ % loop over elements and sum fields for ind = 1:num_elements - % get complex pressure for bowls with inner and outer aperature diam + % get complex pressure for bowls with inner and outer aperture diam if diameter(1, ind) == 0 p_el_inner = 0; else diff --git a/+kwave/+legacy/getAlphaFilter.m b/+kwave/+legacy/getAlphaFilter.m index 4a57139d..7b183304 100644 --- a/+kwave/+legacy/getAlphaFilter.m +++ b/+kwave/+legacy/getAlphaFilter.m @@ -98,7 +98,7 @@ end end -% extract the maximium sound speed +% extract the maximum sound speed c = max(medium.sound_speed(:)); % calculate the alpha_filter size in the z direction for 3D data diff --git a/+kwave/+legacy/getFDMatrix.m b/+kwave/+legacy/getFDMatrix.m index 7d9dc014..bdb02f81 100644 --- a/+kwave/+legacy/getFDMatrix.m +++ b/+kwave/+legacy/getFDMatrix.m @@ -89,7 +89,7 @@ error('Input for accuracy_order must be an integer multiple of 2.'); end -% compute the size of the FD stencil width needed for the given accuarcy +% compute the size of the FD stencil width needed for the given accuracy % and derivative order stencil_width = accuracy_order + (deriv_order + rem(deriv_order, 2)) - 1; diff --git a/+kwave/+legacy/getkWavePath.m b/+kwave/+legacy/getkWavePath.m index 56409b55..c2b72b9b 100644 --- a/+kwave/+legacy/getkWavePath.m +++ b/+kwave/+legacy/getkWavePath.m @@ -14,7 +14,7 @@ % OPTIONAL INPUTS: % folder_name - folder name string to append to the pathname % -% Note: folder_name is not checked for existance, the string is +% Note: folder_name is not checked for existence, the string is % simply appended to the pathname with a trailing slash. % % OUTPUTS: diff --git a/+kwave/+legacy/h5compare.m b/+kwave/+legacy/h5compare.m index 1a7ca391..9ed82950 100644 --- a/+kwave/+legacy/h5compare.m +++ b/+kwave/+legacy/h5compare.m @@ -15,7 +15,7 @@ % % INPUTS: % filename1 - filename of the first HDF5 file -% filename2 - filaname of the second HDF5 file +% filename2 - filename of the second HDF5 file % % OUTPUTS: % number_differences - number of differences between the files diff --git a/+kwave/+legacy/kWaveArray.m b/+kwave/+legacy/kWaveArray.m index 311b7b78..1911423c 100644 --- a/+kwave/+legacy/kWaveArray.m +++ b/+kwave/+legacy/kWaveArray.m @@ -15,7 +15,7 @@ % % No information about the actual simulation grid is stored within % objects of the kWaveArray class. The idea is that an array can be -% defined using physical characteristics, and then re-used for many +% defined using physical characteristics, and then reused for many % different simulations. Note, some methods do use the grid information % for calculations, and therefore take an object of the kWaveGrid class % as an input. The implementation assumes that the grid sampling is @@ -51,7 +51,7 @@ % default computational settings. See [1] for more details. % % 'BLITolerance' - Scalar value controlling where the spatial extent -% of the BLI at each point is trunctated as a +% of the BLI at each point is truncated as a % portion of the maximum value (default = 0.05). % 'BLIType' - String controlling the BLI expression that is % used for each point source, either 'sinc' or diff --git a/+kwave/+legacy/kWaveDiffusion.m b/+kwave/+legacy/kWaveDiffusion.m index bc0aff0a..919ffa77 100644 --- a/+kwave/+legacy/kWaveDiffusion.m +++ b/+kwave/+legacy/kWaveDiffusion.m @@ -498,7 +498,7 @@ elseif strcmp(medium.diffusion_coeff_ref, 'min') - % set to minium value + % set to minimum value kdiff.diffusion_coeff_ref = min( kdiff.diffusion_p1(:) .* kdiff.diffusion_p2(:) ); elseif strcmp(medium.diffusion_coeff_ref, 'mean') @@ -750,7 +750,7 @@ function takeTimeStep(obj, Nt, dt) [deriv_x, deriv_y, deriv_z] = obj.getDerivMatrix(kappa); % compute source update term (this is not dependent on - % temperature, so can be computed once and re-used) + % temperature, so can be computed once and reused) if all(obj.Q == 0) q_term = 0; else @@ -815,7 +815,7 @@ function takeTimeStep(obj, Nt, dt) % initialise movie parameters if obj.record_movie - % force getframe compatability with dual monitors + % force getframe compatibility with dual monitors movegui(img); % create new VideoWriter object (this is supported from diff --git a/+kwave/+legacy/kWaveGrid.m b/+kwave/+legacy/kWaveGrid.m index e1b02c5b..59b06b5a 100644 --- a/+kwave/+legacy/kWaveGrid.m +++ b/+kwave/+legacy/kWaveGrid.m @@ -72,7 +72,7 @@ % % METHODS - MAKETIME: % makeTime automatically specifies Nt, dt, and t_array based on the -% Courant-Friedrichs-Lewy (CFL) number and the grid size. The sytax is: +% Courant-Friedrichs-Lewy (CFL) number and the grid size. The syntax is: % % .makeTime(sound_speed) % .makeTime(sound_speed, cfl) @@ -520,7 +520,7 @@ z_size = obj.Nz .* obj.dz; end - % calulate t_array based on Nt and dt + % calculate t_array based on Nt and dt function t_array = get.t_array(obj) if strcmp(obj.Nt, 'auto') || strcmp(obj.dt, 'auto') t_array = 'auto'; @@ -663,7 +663,7 @@ function setTime(obj, Nt, dt) % note, this produces a circular assignment, where the makeTime % method creates t_array, which is then copied to the t_array % of the same object, however, this allows backwards - % compatability + % compatibility if nargout == 2 t_array = obj.t_array; dt = obj.dt; diff --git a/+kwave/+legacy/kWaveTransducer.m b/+kwave/+legacy/kWaveTransducer.m index 586abf9a..17ac9a43 100644 --- a/+kwave/+legacy/kWaveTransducer.m +++ b/+kwave/+legacy/kWaveTransducer.m @@ -242,7 +242,7 @@ % indicates the transducer element that each grid point belongs to. indexed_elements_mask; - % pseudonym for the active_elements_mask to maintain compatability + % pseudonym for the active_elements_mask to maintain compatibility % when the transducer is used in place of sensor or source. mask; @@ -629,7 +629,7 @@ end - % allow mask query to allow compatability with regular sensor + % allow mask query to allow compatibility with regular sensor % structure - return the active sensor mask function mask = get.mask(obj) mask = obj.active_elements_mask; @@ -897,7 +897,7 @@ % check the data is the correct size if size(sensor_data, 1) ~= (obj.number_active_elements * obj.element_width * obj.element_length) - error('The number of time series in the input sensor_data must match the number of grid points in the active tranducer elements.'); + error('The number of time series in the input sensor_data must match the number of grid points in the active transducer elements.'); end % get index of which element each time series belongs to diff --git a/+kwave/+legacy/kspaceFirstOrder1D.m b/+kwave/+legacy/kspaceFirstOrder1D.m index 5527f91d..cc66030e 100644 --- a/+kwave/+legacy/kspaceFirstOrder1D.m +++ b/+kwave/+legacy/kspaceFirstOrder1D.m @@ -594,7 +594,7 @@ % start time loop for t_index = index_start:index_step:index_end - % enforce time reversal bounday condition + % enforce time reversal boundary condition if flags.time_rev % load pressure value and enforce as a Dirichlet boundary condition diff --git a/+kwave/+legacy/kspaceFirstOrder2D.m b/+kwave/+legacy/kspaceFirstOrder2D.m index 38ac9b37..e67feb8c 100644 --- a/+kwave/+legacy/kspaceFirstOrder2D.m +++ b/+kwave/+legacy/kspaceFirstOrder2D.m @@ -711,7 +711,7 @@ % start time loop for t_index = index_start:index_step:index_end - % enforce time reversal bounday condition + % enforce time reversal boundary condition if flags.time_rev % load pressure value and enforce as a Dirichlet boundary condition diff --git a/+kwave/+legacy/kspaceFirstOrder3D.m b/+kwave/+legacy/kspaceFirstOrder3D.m index 12ed7ff0..4294a37b 100644 --- a/+kwave/+legacy/kspaceFirstOrder3D.m +++ b/+kwave/+legacy/kspaceFirstOrder3D.m @@ -768,7 +768,7 @@ % start time loop for t_index = index_start:index_step:index_end - % enforce time reversal bounday condition + % enforce time reversal boundary condition if flags.time_rev % load pressure value and enforce as a Dirichlet boundary condition @@ -891,7 +891,7 @@ if flags.transducer_source >= t_index % as only flat transducers are currently supported, assume all the - % energy is transfered to x-direction velocity, multiply source + % energy is transferred to x-direction velocity, multiply source % terms by apodization weights ux_sgx(u_source_pos_index) = ux_sgx(u_source_pos_index) + transducer_transmit_apodization.*transducer_input_signal(delay_mask); diff --git a/+kwave/+legacy/kspaceFirstOrderAS.m b/+kwave/+legacy/kspaceFirstOrderAS.m index 4b84e7cc..b49a3013 100644 --- a/+kwave/+legacy/kspaceFirstOrderAS.m +++ b/+kwave/+legacy/kspaceFirstOrderAS.m @@ -693,7 +693,7 @@ % start time loop for t_index = index_start:index_step:index_end - % enforce time reversal bounday condition + % enforce time reversal boundary condition if flags.time_rev % load pressure value and enforce as a Dirichlet boundary condition diff --git a/+kwave/+legacy/kspaceLineRecon.m b/+kwave/+legacy/kspaceLineRecon.m index 8e64797a..8d29a8a7 100644 --- a/+kwave/+legacy/kspaceLineRecon.m +++ b/+kwave/+legacy/kspaceLineRecon.m @@ -209,7 +209,7 @@ % data (first approximation to correcting the limited view problem) p = 2 * 2 * p ./ c; -% enfore positivity condition +% enforce positivity condition if positivity_cond disp(' applying positivity condition...'); p(p < 0) = 0; diff --git a/+kwave/+legacy/kspacePlaneRecon.m b/+kwave/+legacy/kspacePlaneRecon.m index c74e20f9..0d9e8977 100644 --- a/+kwave/+legacy/kspacePlaneRecon.m +++ b/+kwave/+legacy/kspacePlaneRecon.m @@ -212,7 +212,7 @@ % data (first approximation to correcting the limited view problem) (p_zxy) p = 2 * 2 * p ./ c; -% enfore positivity condition (p_zxy) +% enforce positivity condition (p_zxy) if positivity_cond disp(' applying positivity condition...'); p(p < 0) = 0; diff --git a/+kwave/+legacy/kspaceSecondOrder.m b/+kwave/+legacy/kspaceSecondOrder.m index 3f9faa1b..ee40b79e 100644 --- a/+kwave/+legacy/kspaceSecondOrder.m +++ b/+kwave/+legacy/kspaceSecondOrder.m @@ -34,7 +34,7 @@ % Compared to the first-order simulation functions kspaceFirstOrder1D, % kspaceFirstOrder2D, and kspaceFirstOrder3D, kspaceSecondOrder is % restricted to homogeneous media and has less functionality. However, -% it is exact for a homogenous absorbing medium, is more +% it is exact for a homogeneous absorbing medium, is more % computationally efficient, and allows an initial pressure gradient to % be specified. % diff --git a/+kwave/+legacy/makeCartArc.m b/+kwave/+legacy/makeCartArc.m index a2d3e431..76dbe62d 100644 --- a/+kwave/+legacy/makeCartArc.m +++ b/+kwave/+legacy/makeCartArc.m @@ -120,7 +120,7 @@ %COMPUTELINEARTRANSFORM Compute a linear transformation. % % DESCRIPTION: -% computeLinearTransform calculates a rotation matrix to tranform the +% computeLinearTransform calculates a rotation matrix to transform the % computed arc points to the orientation specified by the arc and focus % positions. % diff --git a/+kwave/+legacy/makeSphere.m b/+kwave/+legacy/makeSphere.m index 08174b27..5d254539 100644 --- a/+kwave/+legacy/makeSphere.m +++ b/+kwave/+legacy/makeSphere.m @@ -77,7 +77,7 @@ sphere = zeros(Nx, Ny, Nz); end -% create a guide circle from which the individal radii can be extracted +% create a guide circle from which the individual radii can be extracted guide_circle = makeCircle(Ny, Nx, cy, cx, radius); % step through the guide circle points and create partially filled discs diff --git a/+kwave/+legacy/offGridPoints.m b/+kwave/+legacy/offGridPoints.m index cef47545..f7f5c25e 100644 --- a/+kwave/+legacy/offGridPoints.m +++ b/+kwave/+legacy/offGridPoints.m @@ -52,7 +52,7 @@ % 'MaskOnly' - Boolean controlling whether a logical mask is % returned instead of the non-binary source mask, % where the mask contains the extent of the -% off-grid source (defaul = false). +% off-grid source (default = false). % 'SinglePrecision' - Boolean controlling whether the mask is returned % in single precision. If 'BLITolerance' > 0, then % calculations are also performed in single diff --git a/+kwave/+legacy/private/checkFieldNames.m b/+kwave/+legacy/private/checkFieldNames.m index a2a00ec1..5521ceb7 100644 --- a/+kwave/+legacy/private/checkFieldNames.m +++ b/+kwave/+legacy/private/checkFieldNames.m @@ -1,5 +1,5 @@ function checkFieldNames(structure, field_names) -%CHECKFIELDNAMES Checks structure field names for existance. +%CHECKFIELDNAMES Checks structure field names for existence. % % DESCRIPTION: % checkFieldNames checks the field names of a MATLAB structure against diff --git a/+kwave/+legacy/private/enforceFields.m b/+kwave/+legacy/private/enforceFields.m index 210bd80f..de313443 100644 --- a/+kwave/+legacy/private/enforceFields.m +++ b/+kwave/+legacy/private/enforceFields.m @@ -1,8 +1,8 @@ function enforceFields(structure, field_names) -%ENFORCEFIELDS Check structure field names for existance. +%ENFORCEFIELDS Check structure field names for existence. % % DESCRIPTION: -% enforceFields checks a MATLAB structure for the existance of a set of +% enforceFields checks a MATLAB structure for the existence of a set of % required field name defined by a cell array. If a field name within % the cell array is not found within the structure, an error is thrown % with the name of the missing field given in the error message. The diff --git a/+kwave/+legacy/private/gridDataFast2D.m b/+kwave/+legacy/private/gridDataFast2D.m index bb256836..c3f4ccb8 100644 --- a/+kwave/+legacy/private/gridDataFast2D.m +++ b/+kwave/+legacy/private/gridDataFast2D.m @@ -54,7 +54,7 @@ end -% catch trinagulation error +% catch triangulation error if isempty(tri) error('Data cannot be triangulated.'); end diff --git a/+kwave/+legacy/private/gridDataFast2D_tsearch.m b/+kwave/+legacy/private/gridDataFast2D_tsearch.m index 1aeb868b..53819a78 100644 --- a/+kwave/+legacy/private/gridDataFast2D_tsearch.m +++ b/+kwave/+legacy/private/gridDataFast2D_tsearch.m @@ -46,7 +46,7 @@ % triangulize the data tri = delaunayn([x, y]); -% catch trinagulation error +% catch triangulation error if isempty(tri) error('Data cannot be triangulated.'); end diff --git a/+kwave/+legacy/private/gridDataFast3D.m b/+kwave/+legacy/private/gridDataFast3D.m index 1ca7a193..6aa7fec9 100644 --- a/+kwave/+legacy/private/gridDataFast3D.m +++ b/+kwave/+legacy/private/gridDataFast3D.m @@ -55,7 +55,7 @@ end -% catch trinagulation error +% catch triangulation error if isempty(tri) error('Data cannot be triangulated.'); end diff --git a/+kwave/+legacy/private/kspaceFirstOrder_checkOptionalInputs.m b/+kwave/+legacy/private/kspaceFirstOrder_checkOptionalInputs.m index 364e942b..809f4980 100644 --- a/+kwave/+legacy/private/kspaceFirstOrder_checkOptionalInputs.m +++ b/+kwave/+legacy/private/kspaceFirstOrder_checkOptionalInputs.m @@ -184,7 +184,7 @@ case 'MovieType' - % display warning for depracated input + % display warning for deprecated input disp(' WARNING: Optional input ''MovieType'' is no longer supported.'); case 'MovieProfile' diff --git a/+kwave/+legacy/private/kspaceFirstOrder_initialiseMovieParameters.m b/+kwave/+legacy/private/kspaceFirstOrder_initialiseMovieParameters.m index 6b3a6f2c..212d293d 100644 --- a/+kwave/+legacy/private/kspaceFirstOrder_initialiseMovieParameters.m +++ b/+kwave/+legacy/private/kspaceFirstOrder_initialiseMovieParameters.m @@ -25,7 +25,7 @@ import kwave.legacy.* -% force getframe compatability with dual monitors +% force getframe compatibility with dual monitors movegui(img); % create new VideoWriter object (this is supported from MATLAB 2010b) diff --git a/+kwave/+legacy/private/kspaceFirstOrder_inputChecking.m b/+kwave/+legacy/private/kspaceFirstOrder_inputChecking.m index 9b290f3f..4a275aa6 100644 --- a/+kwave/+legacy/private/kspaceFirstOrder_inputChecking.m +++ b/+kwave/+legacy/private/kspaceFirstOrder_inputChecking.m @@ -1223,7 +1223,7 @@ % CHECK KGRID TIME INPUTS % ========================================================================= -% check kgrid for t_array existance, and create if not defined +% check kgrid for t_array existence, and create if not defined if strcmp(kgrid.t_array, 'auto') % check for time reversal mode @@ -1355,7 +1355,7 @@ error('medium.density must be explicitly defined if medium.BonA is specified.'); end -% check sensor compatability options for flags.compute_directivity +% check sensor compatibility options for flags.compute_directivity if flags.use_sensor && kgrid.dim == 2 && flags.compute_directivity && ~flags.binary_sensor_mask && strcmp(cartesian_interp, 'linear') error('sensor directivity fields are only compatible with binary sensor masks or ''CartInterp'' set to ''nearest''.'); end @@ -1404,7 +1404,7 @@ % display a warning only if using WSWS symmetry (not WSWA-FFT) if strncmp(radial_symmetry, 'WSWS', 4) - disp(' WARNING: Optional input ''RadialSymmetry'' changed to ''WSWA'' for compatability with ''SaveToDisk''.'); + disp(' WARNING: Optional input ''RadialSymmetry'' changed to ''WSWA'' for compatibility with ''SaveToDisk''.'); end % update setting diff --git a/+kwave/+legacy/private/kspaceFirstOrder_saveToDisk.m b/+kwave/+legacy/private/kspaceFirstOrder_saveToDisk.m index 74397d62..a8eaa1c1 100644 --- a/+kwave/+legacy/private/kspaceFirstOrder_saveToDisk.m +++ b/+kwave/+legacy/private/kspaceFirstOrder_saveToDisk.m @@ -223,10 +223,10 @@ % ========================================================================= % SOURCE VARIABLES % ========================================================================= -% source modes and indicies +% source modes and indices % - these are only defined if the source flags are > 0 % - the source mode describes whether the source will be added or replaced -% - the source indicies describe which grid points act as the source +% - the source indices describe which grid points act as the source % - the u_source_index is reused for any of the u sources and the transducer source % velocity source @@ -346,7 +346,7 @@ % initial pressure source variable % - this is only defined if the p0 source flag is 1 -% - this defines the initial pressure everywhere (there is no indicies) +% - this defines the initial pressure everywhere (there is no indices) if p0_source_flag p0_source_input = source.p0; variable_list = [variable_list, {'p0_source_input'}]; diff --git a/+kwave/+legacy/private/kspaceFirstOrder_scaleSourceTerms.m b/+kwave/+legacy/private/kspaceFirstOrder_scaleSourceTerms.m index 38b0ccf9..b528d512 100644 --- a/+kwave/+legacy/private/kspaceFirstOrder_scaleSourceTerms.m +++ b/+kwave/+legacy/private/kspaceFirstOrder_scaleSourceTerms.m @@ -59,7 +59,7 @@ else - % compute the scale parameter seperately for each source + % compute the scale parameter separately for each source % position based on the sound speed at that position for p_index = 1:length(source.p(:, 1)) source.p(p_index, :) = source.p(p_index, :) ./ (N .* c0(p_source_pos_index(p_index)).^2); @@ -72,7 +72,7 @@ % create empty matrix grid_point_sep = zeros(size(kgrid.x)); - % compute averaged grid point seperation map, the interior + % compute averaged grid point separation map, the interior % points are calculated using the average distance to all % connected grid points (the edge values are not calculated % assuming there are no source points in the PML) @@ -118,7 +118,7 @@ else - % compute the scale parameter seperately for each source + % compute the scale parameter separately for each source % position based on the sound speed at that position for p_index = 1:length(source.p(:, 1)) source.p(p_index, :) = source.p(p_index, :) .* (2 .* dt ./ (N .* c0(p_source_pos_index(p_index)) .* kgrid.dx)); @@ -133,7 +133,7 @@ % STRESS SOURCES % ========================================================================= -% scale the stress source by 1/N to divide amoungst the split field +% scale the stress source by 1/N to divide amongst the split field % components, and if source.s_mode is not set to 'dirichlet', also scale by % 2*dt*c0/dx to account for the time step and convert to units of % [kg/(m^3 s)] (note dx is used in all dimensions) @@ -149,7 +149,7 @@ else - % compute the scale parameter seperately for each source + % compute the scale parameter separately for each source % position based on the sound speed at that position for s_index = 1:length(source.sxx(:, 1)) source.sxx(s_index, :) = source.sxx(s_index, :) .* (2 .* dt .* c0(s_source_pos_index(s_index)) ./ (N .* kgrid.dx)); @@ -170,7 +170,7 @@ else - % compute the scale parameter seperately for each source + % compute the scale parameter separately for each source % position based on the sound speed at that position for s_index = 1:length(source.syy(:, 1)) source.syy(s_index, :) = source.syy(s_index, :) .* (2 .* dt .* c0(s_source_pos_index(s_index)) ./ (N .* kgrid.dx)); @@ -191,7 +191,7 @@ else - % compute the scale parameter seperately for each source + % compute the scale parameter separately for each source % position based on the sound speed at that position for s_index = 1:length(source.szz(:, 1)) source.szz(s_index, :) = source.szz(s_index, :) .* (2 .* dt .* c0(s_source_pos_index(s_index)) ./ (N .* kgrid.dx)); @@ -212,7 +212,7 @@ else - % compute the scale parameter seperately for each source + % compute the scale parameter separately for each source % position based on the sound speed at that position for s_index = 1:length(source.sxy(:, 1)) source.sxy(s_index, :) = source.sxy(s_index, :) .* (2 .* dt .* c0(s_source_pos_index(s_index)) ./ (N .* kgrid.dx)); @@ -233,7 +233,7 @@ else - % compute the scale parameter seperately for each source + % compute the scale parameter separately for each source % position based on the sound speed at that position for s_index = 1:length(source.sxz(:, 1)) source.sxz(s_index, :) = source.sxz(s_index, :) .* (2 .* dt .* c0(s_source_pos_index(s_index)) ./ (N .* kgrid.dx)); @@ -254,7 +254,7 @@ else - % compute the scale parameter seperately for each source + % compute the scale parameter separately for each source % position based on the sound speed at that position for s_index = 1:length(source.syz(:, 1)) source.syz(s_index, :) = source.syz(s_index, :) .* (2 .* dt .* c0(s_source_pos_index(s_index)) ./ (N .* kgrid.dx)); @@ -291,7 +291,7 @@ % create empty matrix grid_point_sep = zeros(size(kgrid.x)); - % compute averaged grid point seperation map, the interior + % compute averaged grid point separation map, the interior % points are calculated using the average distance to all % connected grid points (the edge values are not calculated % assuming there are no source points in the PML) @@ -320,7 +320,7 @@ else - % compute the scale parameter seperately for each source position + % compute the scale parameter separately for each source position % based on the sound speed at that position for u_index = 1:length(source.ux(:, 1)) source.ux(u_index, :) = source.ux(u_index, :) .* (2 .* c0(u_source_pos_index(u_index)) .* dt ./ kgrid.dx); @@ -341,7 +341,7 @@ else - % compute the scale parameter seperately for each source position + % compute the scale parameter separately for each source position % based on the sound speed at that position for u_index = 1:length(source.uy(:, 1)) source.uy(u_index, :) = source.uy(u_index, :) .* (2 .* c0(u_source_pos_index(u_index)) .* dt ./ kgrid.dy); @@ -361,7 +361,7 @@ else - % compute the scale parameter seperately for each source position + % compute the scale parameter separately for each source position % based on the sound speed at that position for u_index = 1:length(source.uz(:, 1)) source.uz(u_index, :) = source.uz(u_index, :) .* (2 .* c0(u_source_pos_index(u_index)) .* dt ./ kgrid.dz); diff --git a/+kwave/+legacy/private/kspaceFirstOrder_setSoundSpeedRef.m b/+kwave/+legacy/private/kspaceFirstOrder_setSoundSpeedRef.m index 63b16e71..075f9779 100644 --- a/+kwave/+legacy/private/kspaceFirstOrder_setSoundSpeedRef.m +++ b/+kwave/+legacy/private/kspaceFirstOrder_setSoundSpeedRef.m @@ -73,7 +73,7 @@ else % in the k-space elastic case, there are two reference sound speeds for - % the compressional and shear waves, so compute them seperately + % the compressional and shear waves, so compute them separately if isfield(medium, 'sound_speed_ref_compression') if isnumeric(medium.sound_speed_ref_compression) c_ref_compression = medium.sound_speed_ref_compression; diff --git a/+kwave/+legacy/private/tolStar.m b/+kwave/+legacy/private/tolStar.m index cd4c43ac..f150fd1a 100644 --- a/+kwave/+legacy/private/tolStar.m +++ b/+kwave/+legacy/private/tolStar.m @@ -15,7 +15,7 @@ % % INPUTS: % tolerance - Scalar value controlling where the spatial -% extent of the BLI at each point is trunctated +% extent of the BLI at each point is truncated % as a portion of the maximum value. % kgrid - Object of the kWaveGrid class defining the % Cartesian and k-space grid fields. diff --git a/+kwave/+legacy/pstdElastic2D.m b/+kwave/+legacy/pstdElastic2D.m index 90262a94..3d5a0d37 100644 --- a/+kwave/+legacy/pstdElastic2D.m +++ b/+kwave/+legacy/pstdElastic2D.m @@ -203,7 +203,7 @@ % - time varying pressure enforced as a % Dirichlet boundary condition over sensor.mask % -% Note: For a heterogeneous medium, medium.sound_speed_compression, +% Note: For a heterogeneous medium, medium.sound_speed_compression, % medium.sound_speed_shear, and medium.density must be given in matrix form % with the same dimensions as kgrid. For a homogeneous medium, these can be % given as scalar values. @@ -468,12 +468,12 @@ % (2002). 3D heterogeneous staggered-grid finite-difference modeling of % seismic motion with volume harmonic and arithmetic averaging of elastic % moduli and densities. Bulletin of the Seismological Society of America, -% 92(8), 3042–3066. +% 92(8), 3042-3066. % [2] Toyoda, M., Takahashi, D., & Kawai, Y. (2012). Averaged material % parameters and boundary conditions for the vibroacoustic % finite-difference time-domain method with a nonuniform mesh. Acoustical -% Science and Technology, 33(4), 273–276. +% Science and Technology, 33(4), 273-276. % ========================================================================= % PREPARE DERIVATIVE AND PML OPERATORS @@ -550,7 +550,7 @@ dduydxdt = castZeros([kgrid.Nx, kgrid.Ny]); % ** end -% to save memory, the variables noted with a ** do not neccesarily need to +% to save memory, the variables noted with a ** do not necessarily need to % be explicitly stored (they are not needed for update steps). Instead they % could be replaced with a small number of temporary variables that are % reused several times during the time loop. @@ -617,7 +617,7 @@ for t_index = index_start:index_step:index_end % compute the gradients of the stress tensor (these variables do not - % necessaily need to be stored, they could be computed as needed) + % necessarily need to be stored, they could be computed as needed) dsxxdx = real( ifft( bsxfun(@times, ddx_k_shift_pos, fft(sxx_split_x + sxx_split_y, [], 1)), [], 1) ); dsyydy = real( ifft( bsxfun(@times, ddy_k_shift_pos, fft(syy_split_x + syy_split_y, [], 2)), [], 2) ); dsxydx = real( ifft( bsxfun(@times, ddx_k_shift_neg, fft(sxy_split_x + sxy_split_y, [], 1)), [], 1) ); diff --git a/+kwave/+legacy/pstdElastic3D.m b/+kwave/+legacy/pstdElastic3D.m index 5fb3452a..15648fc9 100644 --- a/+kwave/+legacy/pstdElastic3D.m +++ b/+kwave/+legacy/pstdElastic3D.m @@ -544,12 +544,12 @@ % (2002). 3D heterogeneous staggered-grid finite-difference modeling of % seismic motion with volume harmonic and arithmetic averaging of elastic % moduli and densities. Bulletin of the Seismological Society of America, -% 92(8), 3042–3066. +% 92(8), 3042-3066. % [2] Toyoda, M., Takahashi, D., & Kawai, Y. (2012). Averaged material % parameters and boundary conditions for the vibroacoustic % finite-difference time-domain method with a nonuniform mesh. Acoustical -% Science and Technology, 33(4), 273–276. +% Science and Technology, 33(4), 273-276. % ========================================================================= % PREPARE DERIVATIVE AND PML OPERATORS @@ -688,7 +688,7 @@ dduzdzdt = castZeros([kgrid.Nx, kgrid.Ny, kgrid.Nz]); % ** end -% to save memory, the variables noted with a ** do not neccesarily need to +% to save memory, the variables noted with a ** do not necessarily need to % be explicitly stored (they are not needed for update steps). Instead they % could be replaced with a small number of temporary variables that are % reused several times during the time loop. @@ -755,7 +755,7 @@ for t_index = index_start:index_step:index_end % compute the gradients of the stress tensor (these variables do not - % necessaily need to be stored, they could be computed as needed) + % necessarily need to be stored, they could be computed as needed) dsxxdx = real( ifft( bsxfun(@times, ddx_k_shift_pos, fft(sxx_split_x + sxx_split_y + sxx_split_z, [], 1)), [], 1) ); dsyydy = real( ifft( bsxfun(@times, ddy_k_shift_pos, fft(syy_split_x + syy_split_y + syy_split_z, [], 2)), [], 2) ); dszzdz = real( ifft( bsxfun(@times, ddz_k_shift_pos, fft(szz_split_x + szz_split_y + szz_split_z, [], 3)), [], 3) ); diff --git a/+kwave/+legacy/sharpness.m b/+kwave/+legacy/sharpness.m index 1da272c9..78c6821c 100644 --- a/+kwave/+legacy/sharpness.m +++ b/+kwave/+legacy/sharpness.m @@ -7,7 +7,7 @@ % the Brenner gradient which returns the sum of the centered % finite-difference at each matrix element in each Cartesian direction. % Metrics calculated using the Sobel operator or the normalised -% variance can also be returned by setting the input paramater metric. +% variance can also be returned by setting the input parameter metric. % % For further details, see B. E. Treeby, T. K. Varslot, E. Z. Zhang, % J. G. Laufer, and P. C. Beard, "Automatic sound speed selection in diff --git a/+kwave/+legacy/voxelPlot.m b/+kwave/+legacy/voxelPlot.m index 52b0df6a..5990e6bb 100644 --- a/+kwave/+legacy/voxelPlot.m +++ b/+kwave/+legacy/voxelPlot.m @@ -112,7 +112,7 @@ function voxelPlot(mat, varargin) 'black', 'CData', IMAGE_3D_DATA.voxel_patch_CData(voxel_face_num,:), ... 'FaceColor', 'flat'); -% set the tranparency +% set the transparency set(hp2, 'FaceAlpha', transparency); % set the axes properties and colormap diff --git a/+kwave/+legacy/writeAttributes.m b/+kwave/+legacy/writeAttributes.m index 0bfb6d87..a8862876 100644 --- a/+kwave/+legacy/writeAttributes.m +++ b/+kwave/+legacy/writeAttributes.m @@ -46,7 +46,7 @@ function writeAttributes(filename, file_description) % get literals getH5Literals; -% get computer infor +% get computer info comp_info = getComputerInfo; % set file description if not provided by user diff --git a/+kwave/+tests/+linting/TestCodeQuality.m b/+kwave/+tests/+linting/TestCodeQuality.m index 901f11e4..1706916f 100644 --- a/+kwave/+tests/+linting/TestCodeQuality.m +++ b/+kwave/+tests/+linting/TestCodeQuality.m @@ -5,13 +5,29 @@ % Code quality tests for the k-wave II code base. % %% Description -% The following tests are performed for all .m files in +kwave/+toolbox: +% The following tests are performed for all .m files in |+kwave/+toolbox|: % % # |codeIssues| is called to make sure there are no code quality % errors in the files. % # A dependency check is performed to make sure the files don't depend on % any MATLAB toolboxes. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef TestCodeQuality < matlab.unittest.TestCase properties @@ -73,7 +89,7 @@ function checkDependencies(testCase) function testCyclomaticComplexity(~) % Get list of changed files. - changedFiles = kwave.utilities.gitChangedFiles(true); + changedFiles = kwave.devtools.gitChangedFiles(true); % Exclude any files not in the toolbox folder. changedFiles(~contains(changedFiles, '+toolbox')) = []; @@ -98,7 +114,7 @@ function testCyclomaticComplexity(~) % Calculate complexity. msg = strtrim(checkcode(changedFiles{ind}, "-cyc", '-string')); - + % Append the message to the file. fprintf(fid, '%s\n', msg); diff --git a/+kwave/+tests/+unit/AbstractTestGrid.m b/+kwave/+tests/+unit/AbstractTestGrid.m index d9b68e84..0bde86a5 100644 --- a/+kwave/+tests/+unit/AbstractTestGrid.m +++ b/+kwave/+tests/+unit/AbstractTestGrid.m @@ -22,17 +22,17 @@ % classdef TestMyClass < kwave.tests.unit.AbstractTestGrid % methods(Test, ParameterCombination="sequential") % function testSomething(testCase) -% +% % % import matlab.unittest.constraints.IsEqualTo % import kwave.toolbox.* -% +% % % % Assign actual and reference solutions. % testCase.actualSolution = testCase.kgrid.xVec; % testCase.referenceSolution = testCase.kgrid.yVec; -% +% % % % Compare with tolerance. % testCase.verifyThat(testCase.actualSolution, IsEqualTo(testCase.referenceSolution, "Within", testCase.tol)); -% +% % % end % end % end @@ -40,6 +40,22 @@ %% See Also % * |AbstractTestGridInput| +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef(Abstract) AbstractTestGrid < matlab.unittest.TestCase properties diff --git a/+kwave/+tests/+unit/AbstractTestGridInput.m b/+kwave/+tests/+unit/AbstractTestGridInput.m index 2a07f2bd..c70d4da6 100644 --- a/+kwave/+tests/+unit/AbstractTestGridInput.m +++ b/+kwave/+tests/+unit/AbstractTestGridInput.m @@ -2,11 +2,11 @@ % *Package:* kwave.tests.unit % *Superclasses:* matlab.unittest.TestCase % -% Superclass for medium unit tests. +% Superclass for GridInput unit tests. % %% Description -% Abstract test class for testing medium property classes derived from -% |GridInput|. Derived classes must define four abstract properties. The +% Abstract test class for testing property classes derived from +% |GridInput|. Derived classes must define six abstract properties. The % first defines the medium class name, and the others define cell arrays of % the medium properties names for the class under test: % @@ -28,7 +28,7 @@ % {'soundSpeedReference'}|. % * |inputPropertiesVectorField| - Cell array of property names for values that % must be vector fields. For example, |inputPropertiesScalar = -% {'initialElectricField'}|. +% {'initialVelocity'}|. % % Derived classes must also contain a test methods block (which can be % empty) so that the tests run: @@ -40,6 +40,22 @@ % property assignment, padded and non-padded size checks, and error states % for grid sizes in 1D, 2D, and 3D. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef(Abstract) AbstractTestGridInput < matlab.unittest.TestCase properties(Abstract) @@ -78,7 +94,7 @@ function createGrid(testCase, gridSize, gridSpacing, gridPadding) methods - % Checks if a property must be complex, to e.g. help setting test + % Checks if a property must be complex, to e.g. help setting test % values. function isComplex = mustBeComplex(testCase, propertyName) isComplex = ismember(propertyName, testCase.inputPropertiesComplex); @@ -99,7 +115,7 @@ function createGrid(testCase, gridSize, gridSpacing, gridPadding) isComplex = testCase.mustBeComplex(propertyName); if gridSize == 1 - val = rand + 1i .* rand .* isComplex; + val = rand + 1i .* rand .* isComplex; else if testCase.isVectorField(propertyName) gridSize(4) = testCase.kgrid.dimensions; @@ -107,7 +123,7 @@ function createGrid(testCase, gridSize, gridSpacing, gridPadding) val = rand(gridSize) + 1i .* isComplex; end end - + end methods(Test) @@ -199,4 +215,4 @@ function testCustomDisplay(testCase) end -end \ No newline at end of file +end diff --git a/+kwave/+tests/+unit/TestAcousticMedium.m b/+kwave/+tests/+unit/TestAcousticMedium.m index 9c52b74c..fdd3e743 100644 --- a/+kwave/+tests/+unit/TestAcousticMedium.m +++ b/+kwave/+tests/+unit/TestAcousticMedium.m @@ -4,8 +4,25 @@ % % Unit tests for the AcousticMedium class using the TestMedium class. % -% *Verifies the Acoustic solver will not run with a medium not of the -% medium or acoustic medium type. +%% Description +% Verifies the Acoustic solver will not run with a medium not of the +% medium or acoustic medium type. + +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . classdef TestAcousticMedium < kwave.tests.unit.AbstractTestGridInput @@ -31,7 +48,7 @@ function testMissingProperties(testCase) source = AcousticSource(kgrid); testCase.verifyError(@() AcousticSolver(kgrid, medium, source, []), 'AcousticSolver:InvalidMediumType'); end - end + end end diff --git a/+kwave/+tests/+unit/TestAcousticSensor.m b/+kwave/+tests/+unit/TestAcousticSensor.m new file mode 100644 index 00000000..9ac6c236 --- /dev/null +++ b/+kwave/+tests/+unit/TestAcousticSensor.m @@ -0,0 +1,166 @@ +%% TestAcousticSensor +% *Package:* kwave.tests.unit +% *Superclasses:* matlab.unittest.TestCase +% +% Unit tests for the AcousticSensor class. +% +%% Description +% Tests shape allocation and basic property behavior for AcousticSensor. +% The tests avoid relying on the full solver. + +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + +classdef TestAcousticSensor < matlab.unittest.TestCase + + properties + grid % kwave.toolbox.Grid + sensor % kwave.toolbox.AcousticSensor + end + + methods (TestMethodSetup) + function createGridAndSensor(testCase) + + % 2D grid; small for quick tests. + testCase.grid = kwave.toolbox.Grid([8, 7], [1, 1]); + + % Underlying Sensor constructor typically accepts the grid. + testCase.sensor = kwave.toolbox.AcousticSensor(testCase.grid); + + % Define a sparse mask with a known number of points. + mask = false(testCase.grid.gridSize); + mask(1,1) = true; + mask(3,5) = true; + mask(8,7) = true; + testCase.sensor.mask = mask; % totalSensorPoints should be nnz(mask) + % Leave timeSteps at default (1) unless a test changes it. + end + end + + methods (Test) + + function defaults_are_as_documented(testCase) + s = testCase.sensor; + testCase.verifyEqual(s.pressureSensor, 'on'); % default + testCase.verifyEqual(s.velocitySensor, 'off'); % default + testCase.verifyEqual(s.densitySensor, 'off'); % default + testCase.verifyEqual(s.timeSteps, 1); % default + end + + function validators_reject_invalid_members(testCase) + s = testCase.sensor; + testCase.verifyError(@() setVelocity(s, 'bad'), 'MATLAB:validators:mustBeMember'); + testCase.verifyError(@() setPressure(s, 'maybe'), 'MATLAB:validators:mustBeMember'); + testCase.verifyError(@() setDensity(s, 'maybe'), 'MATLAB:validators:mustBeMember'); + function setVelocity(obj,val), obj.velocitySensor = val; end + function setPressure(obj,val), obj.pressureSensor = val; end + function setDensity(obj,val), obj.densitySensor = val; end + end + + function initialise_Nt0_pressure_only(testCase) + s = testCase.sensor; + s.pressureSensor = 'on'; + s.densitySensor = 'off'; + s.velocitySensor = 'off'; + s = s.initialiseSensorData(0); + nPts = s.totalSensorPoints; + testCase.verifySize(s.pressure, [nPts, 1]); + testCase.verifyEqual(s.times, 0); + end + + function initialise_Nt0_density_only_allocates_density(testCase) + % This test expects density to allocate at Nt == 0 when densitySensor='on'. + s = testCase.sensor; + s.pressureSensor = 'off'; + s.densitySensor = 'on'; + s.velocitySensor = 'off'; + s = s.initialiseSensorData(0); + nPts = s.totalSensorPoints; + testCase.verifySize(s.density, [nPts, 1]); % Expected behavior + testCase.verifyTrue(isempty(s.pressure)); % Should not allocate pressure here + testCase.verifyEqual(s.times, 0); + end + + function initialise_Nt0_velocity_on_shape(testCase) + % Expect velocity to allocate [nPts, D, 1] so it matches the + % recordSensorData slicing convention velocity(:,:,n) + s = testCase.sensor; + s.pressureSensor = 'off'; + s.densitySensor = 'off'; + s.velocitySensor = 'on'; + s = s.initialiseSensorData(0); + nPts = s.totalSensorPoints; + D = s.kgrid.dimensions; + testCase.verifySize(s.velocity, [nPts, D, 1]); % Expected + testCase.verifyEqual(s.times, 0); + end + + function initialise_positiveNt_allocates_all_and_times(testCase) + s = testCase.sensor; + s.pressureSensor = 'on'; + s.densitySensor = 'on'; + s.velocitySensor = 'on'; + s.timeStepSpacing = 2; + Nt = 9; % arbitrary + nCols = floor(Nt/s.timeStepSpacing) + 1; % 5 + s = s.initialiseSensorData(Nt); + nPts = s.totalSensorPoints; + D = s.kgrid.dimensions; + testCase.verifySize(s.pressure, [nPts, nCols]); + testCase.verifySize(s.density, [nPts, nCols]); + testCase.verifySize(s.velocity, [nPts, D, nCols]); + testCase.verifySize(s.times, [1, nCols]); + end + + function initialise_extend_buffers_on_subsequent_calls(testCase) + % First call allocates; second call should extend by floor(Nt/timeSteps). + s = testCase.sensor; + s.pressureSensor = 'on'; + s.timeStepSpacing = 2; + s = s.initialiseSensorData(4); % nCols = 3 + s = s.initialiseSensorData(4); % extend by floor(4/2)=2 -> total 5 + nCols = 5; + nPts = s.totalSensorPoints; + testCase.verifySize(s.pressure, [nPts, nCols]); + testCase.verifySize(s.times, [1, nCols]); + end + + % function record_velocity_on_grid_branch_respected(testCase) + % % Demonstrate how to test 'ongrid' recording using a spy that bypasses + % % ProcessSensorData. This is a template; enable when ready. + % % + % % NOTE: The attached class compares 'onGrid' (camel case) in recordSensorData, + % % while the validator allows 'ongrid' (lowercase). This test expects lowercase + % % to be honored; if the class isn't case-normalizing, it may fail and surface + % % the inconsistency. + % testCase.assumeTrue(true); % set to false if you want to skip for now + % + % s = testCase.sensor; + % s.pressureSensor = 'off'; + % s.velocitySensor = 'ongrid'; % per validator + % s = s.initialiseSensorData(3); + % + % spy = kwave.tests.unit.support.AcousticSensorSpy(s); % see helper class below + % + % fake = kwave.tests.unit.support.FakeSolver(testCase.grid); + % fake.timePoint = 1.23; + % + % spy = spy.recordSensorData(fake, 1); + % testCase.verifySize(spy.velocity, [spy.totalSensorPoints, spy.kgrid.dimensions, size(spy.velocity,3)]); + % testCase.verifyEqual(spy.times(1), fake.timePoint); + % end + end +end diff --git a/+kwave/+tests/+unit/TestAcousticSolver.m b/+kwave/+tests/+unit/TestAcousticSolver.m index a9522240..cbdfd23f 100644 --- a/+kwave/+tests/+unit/TestAcousticSolver.m +++ b/+kwave/+tests/+unit/TestAcousticSolver.m @@ -8,15 +8,37 @@ % %% Description % Runs the following tests for the AcousticSolver: +% % * Verifies that plane wave simulations in 2D and 3D match simulations in % 1D. Performed both with constant and nonconstant gridfield variables. % * Tests that the simulations produce the same results as the legacy code. -% * Verfifies than the simulations produce the same result when using the +% * Verifies than the simulations produce the same result when using the % medium class or acoustic medium class -% * Tests the numerical solution for the pressure when given both an -% initial velocity and initial pressure in 1D. +% * Verifies the numerical solution for the pressure when given both an +% initial velocity and initial pressure in 1D. % * Verifies the plane waves when an initial velocity is given in 1D, 2D -% and 3D, with consideration for directional velocity. +% and 3D, with consideration for directional velocity. +% * Verifies automatic creation of Nt and dt when required. +% * Verifies sensorData is initialised when a sensor is defined. +% * Verifies that an error occurs if the medium, source, or sensor grids do +% not equal the computational grid. + +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef TestAcousticSolver < matlab.unittest.TestCase @@ -33,16 +55,16 @@ function testPlaneWaves(testCase) tol = matlab.unittest.constraints.AbsoluteTolerance(single(1e-6)); % Test properties. - Nax = 256; - Nlat = 16; - dx = 4e-3; - c0 = 1500; - rho0 = 1000; - alpha0=10; + Nax = 256; + Nlat = 16; + dx = 4e-3; + c0 = 1500; + rho0 = 1000; + alpha0 = 10; pmlSize = 20; - CFL = 0.5; - Nt = 150; - dt = CFL * dx / c0; + CFL = 0.3; + Nt = 150; + dt = CFL * dx / c0; % Settings. settings = Settings; @@ -50,32 +72,32 @@ function testPlaneWaves(testCase) % Construct Mediums - kgrid1D = Grid(Nax, dx, pmlSize); + kgrid1D = Grid(Nax, dx, pmlSize); medium1D = AcousticMedium(kgrid1D); source1D = AcousticSource(kgrid1D); source1D.initialPressure = exp( -(kgrid1D.xVec - 25e-3).^2 ./ (5 * kgrid1D.dx).^2 ); - kgrid2Dx = Grid([Nax, Nlat], dx, [pmlSize, 0]); + kgrid2Dx = Grid([Nax, Nlat], dx, [pmlSize, 0]); medium2Dx = AcousticMedium(kgrid2Dx); source2Dx = AcousticSource(kgrid2Dx); source2Dx.initialPressure = repmat(source1D.initialPressure, [1, Nlat]); - kgrid2Dy = Grid([Nlat, Nax], dx, [0, pmlSize]); + kgrid2Dy = Grid([Nlat, Nax], dx, [0, pmlSize]); medium2Dy = AcousticMedium(kgrid2Dy); source2Dy = AcousticSource(kgrid2Dy); source2Dy.initialPressure = repmat(reshape(source1D.initialPressure, 1, []), [Nlat, 1]); - kgrid3Dx = Grid([Nax, Nlat, Nlat], dx, [pmlSize, 0, 0]); + kgrid3Dx = Grid([Nax, Nlat, Nlat], dx, [pmlSize, 0, 0]); medium3Dx = AcousticMedium(kgrid3Dx); source3Dx = AcousticSource(kgrid3Dx); source3Dx.initialPressure = repmat(source1D.initialPressure, [1, Nlat, Nlat]); - kgrid3Dy = Grid([Nlat, Nax, Nlat], dx, [0,pmlSize, 0]); + kgrid3Dy = Grid([Nlat, Nax, Nlat], dx, [0,pmlSize, 0]); medium3Dy = AcousticMedium(kgrid3Dy); source3Dy = AcousticSource(kgrid3Dy); source3Dy.initialPressure = repmat(reshape(source1D.initialPressure, 1, [], 1), [Nlat, 1, Nlat]); - kgrid3Dz = Grid([Nlat, Nlat, Nax], dx, [0, 0, pmlSize]); + kgrid3Dz = Grid([Nlat, Nlat, Nax], dx, [0, 0, pmlSize]); medium3Dz = AcousticMedium(kgrid3Dz); source3Dz = AcousticSource(kgrid3Dz); source3Dz.initialPressure = repmat(reshape(source1D.initialPressure, 1, 1, []), [Nlat, Nlat, 1]); @@ -86,12 +108,12 @@ function testPlaneWaves(testCase) if Type==1 % case 1: no absorption absorptionType='off'; elseif Type==2 % case 2: absorptionPower - medium1D.absorptionPower = 1.9; - medium2Dx.absorptionPower= 1.9; - medium2Dy.absorptionPower= 1.9; - medium3Dx.absorptionPower= 1.9; - medium3Dy.absorptionPower= 1.9; - medium3Dz.absorptionPower= 1.9; + medium1D.absorptionPower = 1.9; + medium2Dx.absorptionPower = 1.9; + medium2Dy.absorptionPower = 1.9; + medium3Dx.absorptionPower = 1.9; + medium3Dy.absorptionPower = 1.9; + medium3Dz.absorptionPower = 1.9; absorptionType='on'; elseif Type==3 % case 3: absorptionPower noAbsorption absorptionType='noAbsorption'; @@ -227,42 +249,42 @@ function testPlaneWaves(testCase) solver1D.absorptionType=absorptionType; solver1D.run(Nt=Nt, dt=dt); pressure1D = solver1D.pressure; - density1D = solver1D.densitySplit; + density1D = solver1D.densitySplit; velocity1D = solver1D.velocity; solver2Dx = AcousticSolver(kgrid2Dx, medium2Dx, source2Dx, [], settings); solver2Dx.absorptionType=absorptionType; solver2Dx.run(Nt=Nt, dt=dt); pressure2Dx = squeeze(solver2Dx.pressure(:, end/2)); - density2Dx = squeeze(solver2Dx.densitySplit(:, end/2, 1, 1)); + density2Dx = squeeze(solver2Dx.densitySplit(:, end/2, 1, 1)); velocity2Dx = squeeze(solver2Dx.velocity(:, end/2, 1, 1)); solver2Dy = AcousticSolver(kgrid2Dy, medium2Dy, source2Dy, [], settings); solver2Dy.absorptionType=absorptionType; solver2Dy.run(Nt=Nt, dt=dt); pressure2Dy = reshape(squeeze(solver2Dy.pressure(end/2, :)), [], 1); - density2Dy = reshape(squeeze(solver2Dy.densitySplit(end/2, :, 1, 2)), [], 1); + density2Dy = reshape(squeeze(solver2Dy.densitySplit(end/2, :, 1, 2)), [], 1); velocity2Dy = reshape(squeeze(solver2Dy.velocity(end/2, :, 1, 2)), [], 1); solver3Dx = AcousticSolver(kgrid3Dx, medium3Dx, source3Dx, [], settings); % Failed 1-5 solver3Dx.absorptionType=absorptionType; solver3Dx.run(Nt=Nt, dt=dt); - pressure3Dx = squeeze(solver3Dx.pressure(:, end/2, end/2)); - density3Dx = squeeze(solver3Dx.densitySplit(:, end/2, end/2, 1)); + pressure3Dx = squeeze(solver3Dx.pressure(:, end/2, end/2, 1)); + density3Dx = squeeze(solver3Dx.densitySplit(:, end/2, end/2, 1)); velocity3Dx = squeeze(solver3Dx.velocity(:, end/2, end/2, 1)); solver3Dy = AcousticSolver(kgrid3Dy, medium3Dy, source3Dy, [], settings); solver3Dy.absorptionType=absorptionType; solver3Dy.run(Nt=Nt, dt=dt); pressure3Dy = reshape(squeeze(solver3Dy.pressure(end/2, :, end/2)), [], 1, 1); - density3Dy = reshape(squeeze(solver3Dy.densitySplit(end/2, :, end/2, 2)), [], 1, 1); + density3Dy = reshape(squeeze(solver3Dy.densitySplit(end/2, :, end/2, 2)), [], 1, 1); velocity3Dy = reshape(squeeze(solver3Dy.velocity(end/2, :, end/2, 2)), [], 1, 1); solver3Dz = AcousticSolver(kgrid3Dz, medium3Dz, source3Dz, [], settings); solver3Dz.absorptionType=absorptionType; solver3Dz.run(Nt=Nt, dt=dt); pressure3Dz = reshape(squeeze(solver3Dz.pressure(end/2, end/2, :)), [], 1, 1); - density3Dz = reshape(squeeze(solver3Dz.densitySplit(end/2, end/2, :, 3)), [], 1, 1); + density3Dz = reshape(squeeze(solver3Dz.densitySplit(end/2, end/2, :, 3)), [], 1, 1); velocity3Dz = reshape(squeeze(solver3Dz.velocity(end/2, end/2, :, 3)), [], 1, 1); % Testing @@ -317,10 +339,10 @@ function testAgainstLegacyAbsorption(testCase) dt = CFL * dx / c0; % New setup - kgrid1D = Grid(Nax, dx, pmlSize); + kgrid1D = Grid(Nax, dx, pmlSize); medium1D = AcousticMedium(kgrid1D); - medium1D.soundSpeed = c0; - medium1D.density = rho0; + medium1D.soundSpeed = c0; + medium1D.density = rho0; source1D = AcousticSource(kgrid1D); source1D.initialPressure = exp( -(kgrid1D.xVec - 25e-3).^2 ./ (5 * kgrid1D.dx).^2 ); settings = Settings; @@ -328,51 +350,51 @@ function testAgainstLegacyAbsorption(testCase) solver1D = AcousticSolver(kgrid1D, medium1D, source1D, [], settings); solver1D.run(Nt=Nt, dt=dt); % Legacy setup - kgrid1DL=kWaveGrid(Nax,dx); + kgrid1DL = kWaveGrid(Nax,dx); kgrid1DL.setTime(Nt+1, dt) - source1DL.p0= exp( -((kgrid1DL.x_vec - 25e-3 ).^2) ./ ( 5* kgrid1DL.dx).^2 ) ; - medium1DL.sound_speed=c0; - medium1DL.density=rho0; + source1DL.p0 = exp( -((kgrid1DL.x_vec - 25e-3 ).^2) ./ ( 5* kgrid1DL.dx).^2 ) ; + medium1DL.sound_speed = c0; + medium1DL.density = rho0; sensor1DL.record={'p_final'}; - sensor_data1 = kspaceFirstOrder1D(kgrid1DL, medium1DL, source1DL, sensor1DL,'PMLInside',false,'Smooth',false); + sensor_data1 = kspaceFirstOrder1D(kgrid1DL, medium1DL, source1DL, sensor1DL,'PMLInside',false,'Smooth',false,'PlotSim',false); testCase.verifyThat(solver1D.pressure, IsEqualTo(single(sensor_data1.p_final), "Within", tol)); % Staggered Density rho0M = rho0*ones(Nax,1); - rho0M(floor(2*Nax/5):floor(3*Nax/5))=rho0/1.5; + rho0M(floor(2*Nax/5):floor(3*Nax/5)) = rho0/1.5; medium1D.density = rho0M; - medium1DL.density=rho0M; + medium1DL.density = rho0M; solver1D = AcousticSolver(kgrid1D, medium1D, source1D, [], settings); solver1D.run(Nt=Nt, dt=dt); - sensor_data1 = kspaceFirstOrder1D(kgrid1DL, medium1DL, source1DL, sensor1DL,'PMLInside',false,'Smooth',false); + sensor_data1 = kspaceFirstOrder1D(kgrid1DL, medium1DL, source1DL, sensor1DL,'PMLInside',false,'Smooth',false,'PlotSim',false); testCase.verifyThat(solver1D.pressure, IsEqualTo(single(sensor_data1.p_final), "Within", tol)); % PowerLaw - medium1D.absorptionCoeff = alpha0; - medium1DL.alpha_coeff = alpha0; - medium1D.absorptionPower = 1.9; - medium1DL.alpha_power = 1.9; + medium1D.absorptionCoeff = alpha0; + medium1DL.alpha_coeff = alpha0; + medium1D.absorptionPower = 1.9; + medium1DL.alpha_power = 1.9; solver1D = AcousticSolver(kgrid1D, medium1D, source1D, [], settings); solver1D.absorptionType="on"; solver1D.run(Nt=Nt, dt=dt); - sensor_data1 = kspaceFirstOrder1D(kgrid1DL, medium1DL, source1DL, sensor1DL,'PMLInside',false,'Smooth',false); + sensor_data1 = kspaceFirstOrder1D(kgrid1DL, medium1DL, source1DL, sensor1DL,'PMLInside',false,'Smooth',false,'PlotSim',false); testCase.verifyThat(solver1D.pressure, IsEqualTo(single(sensor_data1.p_final), "Within", tol)); % 2D case no dispersion - kgrid2Dx = Grid([Nax, Nlat], dx, [pmlSize, 0]); + kgrid2Dx = Grid([Nax, Nlat], dx, [pmlSize, 0]); medium2Dx = AcousticMedium(kgrid2Dx); source2Dx = AcousticSource(kgrid2Dx); source2Dx.initialPressure = repmat(source1D.initialPressure, [1, Nlat]); - kgrid2Dy = Grid([Nlat, Nax], dx, [0, pmlSize]); + kgrid2Dy = Grid([Nlat, Nax], dx, [0, pmlSize]); medium2Dy = AcousticMedium(kgrid2Dy); source2Dy = AcousticSource(kgrid2Dy); source2Dy.initialPressure = repmat(reshape(source1D.initialPressure, 1, []), [Nlat, 1]); medium2Dx.soundSpeed = c0; - medium2Dx.density = rho0; - medium2Dx.absorptionCoeff= alpha0; - medium2Dx.absorptionPower= 1.9; + medium2Dx.density = rho0; + medium2Dx.absorptionCoeff = alpha0; + medium2Dx.absorptionPower = 1.9; medium2Dy.soundSpeed = c0; - medium2Dy.density = rho0; + medium2Dy.density = rho0; medium2Dy.absorptionCoeff= alpha0; medium2Dy.absorptionPower= 1.9; solver2Dx = AcousticSolver(kgrid2Dx, medium2Dx, source2Dx, [], settings); @@ -382,51 +404,51 @@ function testAgainstLegacyAbsorption(testCase) solver2Dy.absorptionType='noAbsorption'; solver2Dy.run(Nt=Nt, dt=dt); - kgrid2DxL=kWaveGrid(Nax,dx,Nlat,dx); + kgrid2DxL = kWaveGrid(Nax,dx,Nlat,dx); kgrid2DxL.setTime(Nt+1, dt) - source2DxL.p0= repmat(source1D.initialPressure, [1, Nlat]); - medium2DxL.sound_speed=c0; - medium2DxL.density=rho0; - medium2DxL.alpha_power = 1.9; - medium2DxL.alpha_coeff=alpha0; - sensor2DxL.record={'p_final'}; - medium2DxL.alpha_mode='no_dispersion'; - sensor_data2x = kspaceFirstOrder2D(kgrid2DxL, medium2DxL, source2DxL, sensor2DxL,'PMLInside',false,'Smooth',false,'PMLSize',[pmlSize,0]); - kgrid2DyL=kWaveGrid(Nlat,dx,Nax,dx); + source2DxL.p0 = repmat(source1D.initialPressure, [1, Nlat]); + medium2DxL.sound_speed = c0; + medium2DxL.density = rho0; + medium2DxL.alpha_power = 1.9; + medium2DxL.alpha_coeff = alpha0; + sensor2DxL.record = {'p_final'}; + medium2DxL.alpha_mode = 'no_dispersion'; + sensor_data2x = kspaceFirstOrder2D(kgrid2DxL, medium2DxL, source2DxL, sensor2DxL,'PMLInside',false,'Smooth',false,'PMLSize',[pmlSize,0],'PlotSim',false); + kgrid2DyL = kWaveGrid(Nlat,dx,Nax,dx); kgrid2DyL.setTime(Nt+1, dt) - source2DyL.p0= repmat(reshape(source1D.initialPressure, 1, []), [Nlat, 1]); - medium2DyL.sound_speed=c0; - medium2DyL.density=rho0; - medium2DyL.alpha_power = 1.9; - medium2DyL.alpha_coeff=alpha0; - sensor2DyL.record={'p_final'}; - medium2DyL.alpha_mode='no_absorption'; - sensor_data2y = kspaceFirstOrder2D(kgrid2DyL, medium2DyL, source2DyL, sensor2DyL,'PMLInside',false,'Smooth',false,'PMLSize',[0,pmlSize]); + source2DyL.p0 = repmat(reshape(source1D.initialPressure, 1, []), [Nlat, 1]); + medium2DyL.sound_speed = c0; + medium2DyL.density = rho0; + medium2DyL.alpha_power = 1.9; + medium2DyL.alpha_coeff = alpha0; + sensor2DyL.record = {'p_final'}; + medium2DyL.alpha_mode = 'no_absorption'; + sensor_data2y = kspaceFirstOrder2D(kgrid2DyL, medium2DyL, source2DyL, sensor2DyL,'PMLInside',false,'Smooth',false,'PMLSize',[0,pmlSize],'PlotSim',false); testCase.verifyThat(solver2Dx.pressure, IsEqualTo(single(sensor_data2x.p_final), "Within", tol)); testCase.verifyThat(solver2Dy.pressure, IsEqualTo(single(sensor_data2y.p_final), "Within", tol)); - kgrid3Dx = Grid([Nax, Nlat,Nlat], dx, [pmlSize, 0,0]); + kgrid3Dx = Grid([Nax, Nlat,Nlat], dx, [pmlSize, 0,0]); medium3Dx = AcousticMedium(kgrid3Dx); source3Dx = AcousticSource(kgrid3Dx); source3Dx.initialPressure = repmat(source1D.initialPressure, [1, Nlat, Nlat]); medium3Dx.soundSpeed = c0; - medium3Dx.density = rho0; - medium3Dx.absorptionCoeff= alpha0*0.9; - medium3Dx.absorptionPower= 1.2; + medium3Dx.density = rho0; + medium3Dx.absorptionCoeff = alpha0*0.9; + medium3Dx.absorptionPower = 1.2; solver3Dx = AcousticSolver(kgrid3Dx, medium3Dx, source3Dx, [], settings); - solver3Dx.absorptionType="on"; + solver3Dx.absorptionType = "on"; solver3Dx.run(Nt=Nt, dt=dt); - kgrid3DxL=kWaveGrid(Nax,dx,Nlat,dx,Nlat,dx); + kgrid3DxL = kWaveGrid(Nax,dx,Nlat,dx,Nlat,dx); kgrid3DxL.setTime(Nt+1, dt) - source3DxL.p0= repmat(source1D.initialPressure, [1, Nlat, Nlat]); - medium3DxL.sound_speed=c0; - medium3DxL.density=rho0; - medium3DxL.alpha_power = 1.2; - medium3DxL.alpha_coeff=alpha0*0.9; - sensor3DxL.record={'p_final'}; - sensor_data3x = kspaceFirstOrder3D(kgrid3DxL, medium3DxL, source3DxL, sensor3DxL,'PMLInside',false,'Smooth',false,'PMLSize',[pmlSize,0,0]); + source3DxL.p0 = repmat(source1D.initialPressure, [1, Nlat, Nlat]); + medium3DxL.sound_speed = c0; + medium3DxL.density = rho0; + medium3DxL.alpha_power = 1.2; + medium3DxL.alpha_coeff = alpha0*0.9; + sensor3DxL.record = {'p_final'}; + sensor_data3x = kspaceFirstOrder3D(kgrid3DxL, medium3DxL, source3DxL, sensor3DxL,'PMLInside',false,'Smooth',false,'PMLSize',[pmlSize,0,0],'PlotSim',false); testCase.verifyThat(solver3Dx.pressure, IsEqualTo(single(sensor_data3x.p_final), "Within", tol)); end @@ -439,16 +461,16 @@ function testTimeRestarting(testCase) tol = matlab.unittest.constraints.AbsoluteTolerance(single(1e-6)); % Test properties. - Nax = 256; - Nlat = 16; - dx = 4e-3; - c0 = 1500; - rho0 = 1000; - alpha0=10; + Nax = 256; + Nlat = 16; + dx = 4e-3; + c0 = 1500; + rho0 = 1000; + alpha0 = 10; pmlSize = 20; - CFL = 0.3; % Smaller CFL for 2D and 3D, solutions still accurate with higher but not to Tol - Nt = 150; - dt = CFL * dx / c0; + CFL = 0.3; % Smaller CFL for 2D and 3D, solutions still accurate with higher but not to Tol + Nt = 150; + dt = CFL * dx / c0; % Settings. settings = Settings; @@ -456,32 +478,32 @@ function testTimeRestarting(testCase) % Construct Mediums - kgrid1D = Grid(Nax, dx, pmlSize); + kgrid1D = Grid(Nax, dx, pmlSize); medium1D = AcousticMedium(kgrid1D); source1D = AcousticSource(kgrid1D); source1D.initialPressure = exp( -(kgrid1D.xVec - 25e-3).^2 ./ (5 * kgrid1D.dx).^2 ); - kgrid2Dx = Grid([Nax, Nlat], dx, [pmlSize, 0]); + kgrid2Dx = Grid([Nax, Nlat], dx, [pmlSize, 0]); medium2Dx = AcousticMedium(kgrid2Dx); source2Dx = AcousticSource(kgrid2Dx); source2Dx.initialPressure = repmat(source1D.initialPressure, [1, Nlat]); - kgrid2Dy = Grid([Nlat, Nax], dx, [0, pmlSize]); + kgrid2Dy = Grid([Nlat, Nax], dx, [0, pmlSize]); medium2Dy = AcousticMedium(kgrid2Dy); source2Dy = AcousticSource(kgrid2Dy); source2Dy.initialPressure = repmat(reshape(source1D.initialPressure, 1, []), [Nlat, 1]); - kgrid3Dx = Grid([Nax, Nlat, Nlat], dx, [pmlSize, 0, 0]); + kgrid3Dx = Grid([Nax, Nlat, Nlat], dx, [pmlSize, 0, 0]); medium3Dx = AcousticMedium(kgrid3Dx); source3Dx = AcousticSource(kgrid3Dx); source3Dx.initialPressure = repmat(source1D.initialPressure, [1, Nlat, Nlat]); - kgrid3Dy = Grid([Nlat, Nax, Nlat], dx, [0,pmlSize, 0]); + kgrid3Dy = Grid([Nlat, Nax, Nlat], dx, [0,pmlSize, 0]); medium3Dy = AcousticMedium(kgrid3Dy); source3Dy = AcousticSource(kgrid3Dy); source3Dy.initialPressure = repmat(reshape(source1D.initialPressure, 1, [], 1), [Nlat, 1, Nlat]); - kgrid3Dz = Grid([Nlat, Nlat, Nax], dx, [0, 0, pmlSize]); + kgrid3Dz = Grid([Nlat, Nlat, Nax], dx, [0, 0, pmlSize]); medium3Dz = AcousticMedium(kgrid3Dz); source3Dz = AcousticSource(kgrid3Dz); source3Dz.initialPressure = repmat(reshape(source1D.initialPressure, 1, 1, []), [Nlat, Nlat, 1]); @@ -509,7 +531,7 @@ function testTimeRestarting(testCase) solver1DRef = AcousticSolver(kgrid1D, medium1D, source1D, [], settings); solver1DRef.run(Nt=Nt, dt=dt); pressure1DRef = solver1DRef.pressure; - density1DRef = solver1DRef.densitySplit; + density1DRef = solver1DRef.densitySplit; velocity1DRef = solver1DRef.velocity; % 1D Case uses smaller time steps @@ -517,7 +539,7 @@ function testTimeRestarting(testCase) solver1D.run(Nt=2*Nt/3, dt=dt); solver1D.run(Nt=2*Nt/3, dt=dt/2); pressure1D = solver1D.pressure; - density1D = solver1D.densitySplit; + density1D = solver1D.densitySplit; velocity1D = solver1D.velocity; % 2D Case use larger time steps @@ -525,14 +547,14 @@ function testTimeRestarting(testCase) solver2Dx.run(Nt=2*Nt/3, dt=dt); solver2Dx.run(Nt=Nt/6, dt=2*dt); pressure2Dx = squeeze(solver2Dx.pressure(:, end/2)); - density2Dx = squeeze(solver2Dx.densitySplit(:, end/2, 1, 1)); + density2Dx = squeeze(solver2Dx.densitySplit(:, end/2, 1, 1)); velocity2Dx = squeeze(solver2Dx.velocity(:, end/2, 1, 1)); solver2Dy = AcousticSolver(kgrid2Dy, medium2Dy, source2Dy, [], settings); solver2Dy.run(Nt=2*Nt/3, dt=dt); solver2Dy.run(Nt=Nt/6, dt=2*dt); pressure2Dy = reshape(squeeze(solver2Dy.pressure(end/2, :)), [], 1); - density2Dy = reshape(squeeze(solver2Dy.densitySplit(end/2, :, 1, 2)), [], 1); + density2Dy = reshape(squeeze(solver2Dy.densitySplit(end/2, :, 1, 2)), [], 1); velocity2Dy = reshape(squeeze(solver2Dy.velocity(end/2, :, 1, 2)), [], 1); %3D Case swap between smaller, same and larger @@ -540,58 +562,58 @@ function testTimeRestarting(testCase) solver3Dx.run(Nt=2*Nt/3, dt=dt); solver3Dx.run(Nt=Nt/6, dt=2*dt); pressure3Dx = squeeze(solver3Dx.pressure(:, end/2, end/2)); - density3Dx = squeeze(solver3Dx.densitySplit(:, end/2, end/2, 1)); + density3Dx = squeeze(solver3Dx.densitySplit(:, end/2, end/2, 1)); velocity3Dx = squeeze(solver3Dx.velocity(:, end/2, end/2, 1)); solver3Dy = AcousticSolver(kgrid3Dy, medium3Dy, source3Dy, [], settings); solver3Dy.run(Nt=2*Nt/3, dt=dt); solver3Dy.run(Nt=Nt/3, dt=dt); pressure3Dy = reshape(squeeze(solver3Dy.pressure(end/2, :, end/2)), [], 1, 1); - density3Dy = reshape(squeeze(solver3Dy.densitySplit(end/2, :, end/2, 2)), [], 1, 1); + density3Dy = reshape(squeeze(solver3Dy.densitySplit(end/2, :, end/2, 2)), [], 1, 1); velocity3Dy = reshape(squeeze(solver3Dy.velocity(end/2, :, end/2, 2)), [], 1, 1); solver3Dz = AcousticSolver(kgrid3Dz, medium3Dz, source3Dz, [], settings); solver3Dz.run(Nt=2*Nt/3, dt=dt); solver3Dz.run(Nt=2*Nt/3, dt=dt/2); pressure3Dz = reshape(squeeze(solver3Dz.pressure(end/2, end/2, :)), [], 1, 1); - density3Dz = reshape(squeeze(solver3Dz.densitySplit(end/2, end/2, :, 3)), [], 1, 1); + density3Dz = reshape(squeeze(solver3Dz.densitySplit(end/2, end/2, :, 3)), [], 1, 1); velocity3Dz = reshape(squeeze(solver3Dz.velocity(end/2, end/2, :, 3)), [], 1, 1); % Testing - testCase.verifyThat(pressure1DRef, IsEqualTo(pressure1D, "Within", tol)); % - testCase.verifyThat(density1DRef, IsEqualTo(density1D, "Within", tol)); % - testCase.verifyThat(velocity1DRef, IsEqualTo(velocity1D, "Within", tol)); % + testCase.verifyThat(pressure1DRef, IsEqualTo(pressure1D, "Within", tol)); + testCase.verifyThat(density1DRef, IsEqualTo(density1D, "Within", tol)); + testCase.verifyThat(velocity1DRef, IsEqualTo(velocity1D, "Within", tol)); - testCase.verifyThat(pressure2Dx, IsEqualTo(pressure1DRef, "Within", tol)); % - testCase.verifyThat(density2Dx, IsEqualTo(density1DRef, "Within", tol)); % - testCase.verifyThat(velocity2Dx, IsEqualTo(velocity1DRef, "Within", tol)); % + testCase.verifyThat(pressure2Dx, IsEqualTo(pressure1DRef, "Within", tol)); + testCase.verifyThat(density2Dx, IsEqualTo(density1DRef, "Within", tol)); + testCase.verifyThat(velocity2Dx, IsEqualTo(velocity1DRef, "Within", tol)); - testCase.verifyThat(pressure2Dy, IsEqualTo(pressure1DRef, "Within", tol)); % - testCase.verifyThat(density2Dy, IsEqualTo(density1DRef, "Within", tol)); % - testCase.verifyThat(velocity2Dy, IsEqualTo(velocity1DRef, "Within", tol)); % + testCase.verifyThat(pressure2Dy, IsEqualTo(pressure1DRef, "Within", tol)); + testCase.verifyThat(density2Dy, IsEqualTo(density1DRef, "Within", tol)); + testCase.verifyThat(velocity2Dy, IsEqualTo(velocity1DRef, "Within", tol)); - testCase.verifyThat(pressure3Dx, IsEqualTo(pressure1DRef, "Within", tol)); % - testCase.verifyThat(density3Dx, IsEqualTo(density1DRef, "Within", tol)); % - testCase.verifyThat(velocity3Dx, IsEqualTo(velocity1DRef, "Within", tol)); % + testCase.verifyThat(pressure3Dx, IsEqualTo(pressure1DRef, "Within", tol)); + testCase.verifyThat(density3Dx, IsEqualTo(density1DRef, "Within", tol)); + testCase.verifyThat(velocity3Dx, IsEqualTo(velocity1DRef, "Within", tol)); - testCase.verifyThat(pressure3Dy, IsEqualTo(pressure1DRef, "Within", tol)); % - testCase.verifyThat(density3Dy, IsEqualTo(density1DRef, "Within", tol)); % - testCase.verifyThat(velocity3Dy, IsEqualTo(velocity1DRef, "Within", tol)); % + testCase.verifyThat(pressure3Dy, IsEqualTo(pressure1DRef, "Within", tol)); + testCase.verifyThat(density3Dy, IsEqualTo(density1DRef, "Within", tol)); + testCase.verifyThat(velocity3Dy, IsEqualTo(velocity1DRef, "Within", tol)); - testCase.verifyThat(pressure3Dz, IsEqualTo(pressure1D, "Within", tol)); % - testCase.verifyThat(density3Dz, IsEqualTo(density1D, "Within", tol)); % - testCase.verifyThat(velocity3Dz, IsEqualTo(velocity1D, "Within", tol)); % + testCase.verifyThat(pressure3Dz, IsEqualTo(pressure1D, "Within", tol)); + testCase.verifyThat(density3Dz, IsEqualTo(density1D, "Within", tol)); + testCase.verifyThat(velocity3Dz, IsEqualTo(velocity1D, "Within", tol)); % test to show time restart works with change powerlaw - medium1D.absorptionPower=1.1; - medium1D.absorptionCoeff=0.5; - solver1DP= AcousticSolver(kgrid1D, medium1D, source1D, [], settings); + medium1D.absorptionPower = 1.1; + medium1D.absorptionCoeff = 0.5; + solver1DP = AcousticSolver(kgrid1D, medium1D, source1D, [], settings); solver1DP.absorptionType="on"; solver1DP.run(Nt=2, dt=dt); solver1DP.run(Nt=2, dt=2*dt); end - % Test that the medium class returns the same values as the + % Test that the medium class returns the same values as the % acoustic medium class. function testMediumAgreement(testCase) @@ -602,29 +624,30 @@ function testMediumAgreement(testCase) tol = matlab.unittest.constraints.AbsoluteTolerance(single(1e-6)); % Test properties. - Nax = 256; - Nlat = 16; - dx = 4e-3; - c0 = 1500; - rho0 = 1000; - alpha0=10; - alphay=1.9; + Nax = 256; + Nlat = 16; + dx = 4e-3; + c0 = 1480; + rho0 = 1000; + alpha0 = 0.5; + alphay = 2; pmlSize = 20; - CFL = 0.25; - Nt = 150; - dt = CFL * dx / c0; + CFL = 0.25; + Nt = 150; + dt = CFL * dx / c0; kgrid1D = Grid(Nax, dx, pmlSize); % Construct Acoustic Medium medium1D = AcousticMedium(kgrid1D); - medium1D.soundSpeed = c0; - medium1D.density = rho0; - medium1D.absorptionPower=alphay; - medium1D.absorptionCoeff=alpha0; + medium1D.soundSpeed = c0; + medium1D.density = rho0; + medium1D.absorptionPower = alphay; + medium1D.absorptionCoeff = alpha0; % Construct Medium - mediumMaterial1D=Medium(kgrid1D); - mediumMaterial1D.materialIDGrid = 1; - % + materials = Materials(); + mediumMaterial1D = Medium(kgrid1D,materials); + mediumMaterial1D.materialIndexGrid = uint8(0); + source1D = AcousticSource(kgrid1D); source1D.initialPressure = exp( -(kgrid1D.xVec - 25e-3).^2 ./ (5 * kgrid1D.dx).^2 ); settings = Settings; @@ -637,82 +660,82 @@ function testMediumAgreement(testCase) solver1DB = AcousticSolver(kgrid1D, mediumMaterial1D, source1D, [], settings); solver1DB.run(Nt=Nt, dt=dt); testCase.verifyThat(solver1DA.pressure, IsEqualTo(solver1DB.pressure, "Within", tol)); - - kgrid2Dx = Grid([Nax,Nlat], dx, [pmlSize,0]); + + kgrid2Dx = Grid([Nax,Nlat], dx, [pmlSize,0]); source2Dx = AcousticSource(kgrid2Dx); source2Dx.initialPressure = repmat(source1D.initialPressure, [1, Nlat]); % Construct Acoustic Medium medium2Dx = AcousticMedium(kgrid2Dx); - medium2Dx.soundSpeed = c0; - medium2Dx.density = rho0; - medium2Dx.absorptionPower=alphay; - medium2Dx.absorptionCoeff=alpha0; + medium2Dx.soundSpeed = c0; + medium2Dx.density = rho0; + medium2Dx.absorptionPower = alphay; + medium2Dx.absorptionCoeff = alpha0; % Construct Medium - mediumMaterial2Dx=Medium(kgrid2Dx); - mediumMaterial2Dx.materialIDGrid = 1; + mediumMaterial2Dx=Medium(kgrid2Dx,materials); + mediumMaterial2Dx.materialIndexGrid = uint8(0); % solver2DxA = AcousticSolver(kgrid2Dx, medium2Dx, source2Dx, [], settings); solver2DxB = AcousticSolver(kgrid2Dx, mediumMaterial2Dx, source2Dx, [], settings); - solver2DxA.absorptionType='on'; - solver2DxB.absorptionType='on'; + solver2DxA.absorptionType = 'on'; + solver2DxB.absorptionType = 'on'; solver2DxA.run(Nt=Nt, dt=dt); solver2DxB.run(Nt=Nt, dt=dt); testCase.verifyThat(solver2DxA.pressure, IsEqualTo(solver2DxB.pressure, "Within", tol)); % - kgrid2Dy = Grid([Nlat,Nax], dx, [0,pmlSize]); + kgrid2Dy = Grid([Nlat,Nax], dx, [0,pmlSize]); source2Dy = AcousticSource(kgrid2Dy); source2Dy.initialPressure = repmat(reshape(source1D.initialPressure, 1, []), [Nlat, 1]); % Construct Acoustic Medium medium2Dy = AcousticMedium(kgrid2Dy); - medium2Dy.soundSpeed = c0; - medium2Dy.density = rho0; - medium2Dy.absorptionPower=alphay; - medium2Dy.absorptionCoeff=alpha0; + medium2Dy.soundSpeed = c0; + medium2Dy.density = rho0; + medium2Dy.absorptionPower = alphay; + medium2Dy.absorptionCoeff = alpha0; % Construct Medium - mediumMaterial2Dy=Medium(kgrid2Dy); - mediumMaterial2Dy.materialIDGrid = 1; + mediumMaterial2Dy = Medium(kgrid2Dy,materials); + mediumMaterial2Dy.materialIndexGrid = uint8(0); % solver2DyA = AcousticSolver(kgrid2Dy, medium2Dy, source2Dy, [], settings); solver2DyB = AcousticSolver(kgrid2Dy, mediumMaterial2Dy, source2Dy, [], settings); - solver2DyA.absorptionType='noDispersion'; - solver2DyB.absorptionType='noDispersion'; + solver2DyA.absorptionType = 'noDispersion'; + solver2DyB.absorptionType = 'noDispersion'; solver2DyA.run(Nt=Nt, dt=dt); solver2DyB.run(Nt=Nt, dt=dt); testCase.verifyThat(solver2DyA.pressure, IsEqualTo(solver2DyB.pressure, "Within", tol)); % - kgrid3Dx = Grid([Nax,Nlat,Nlat], dx, [pmlSize,0,0]); + kgrid3Dx = Grid([Nax,Nlat,Nlat], dx, [pmlSize,0,0]); source3Dx = AcousticSource(kgrid3Dx); source3Dx.initialPressure = repmat(source1D.initialPressure, [1, Nlat, Nlat]); % Construct Acoustic Medium medium3Dx = AcousticMedium(kgrid3Dx); - medium3Dx.soundSpeed = c0; - medium3Dx.density = rho0; + medium3Dx.soundSpeed = c0; + medium3Dx.density = rho0; medium3Dx.absorptionPower=alphay; medium3Dx.absorptionCoeff=alpha0; % Construct Medium - mediumMaterial3Dx=Medium(kgrid3Dx); - mediumMaterial3Dx.materialIDGrid = 1; + mediumMaterial3Dx=Medium(kgrid3Dx,materials); + mediumMaterial3Dx.materialIndexGrid = uint8(0); % solver3DxA = AcousticSolver(kgrid3Dx, medium3Dx, source3Dx, [], settings); solver3DxB = AcousticSolver(kgrid3Dx, mediumMaterial3Dx, source3Dx, [], settings); - solver3DxA.absorptionType='noAbsorption'; - solver3DxB.absorptionType='noAbsorption'; + solver3DxA.absorptionType = 'noAbsorption'; + solver3DxB.absorptionType = 'noAbsorption'; solver3DxA.run(Nt=Nt, dt=dt); solver3DxB.run(Nt=Nt, dt=dt); testCase.verifyThat(solver3DxA.pressure, IsEqualTo(solver3DxB.pressure, "Within", tol)); % - kgrid3Dy = Grid([Nlat,Nax,Nlat], dx, [0,pmlSize,0]); + kgrid3Dy = Grid([Nlat,Nax,Nlat], dx, [0,pmlSize,0]); source3Dy = AcousticSource(kgrid3Dy); source3Dy.initialPressure = repmat(reshape(source1D.initialPressure, 1, [], 1), [Nlat, 1, Nlat]); % Construct Acoustic Medium medium3Dy = AcousticMedium(kgrid3Dy); - medium3Dy.soundSpeed = c0; - medium3Dy.density = rho0; - medium3Dy.absorptionPower=alphay; - medium3Dy.absorptionCoeff=alpha0; + medium3Dy.soundSpeed = c0; + medium3Dy.density = rho0; + medium3Dy.absorptionPower = alphay; + medium3Dy.absorptionCoeff = alpha0; % Construct Medium - mediumMaterial3Dy=Medium(kgrid3Dy); - mediumMaterial3Dy.materialIDGrid = ones(kgrid3Dy.gridSize); + mediumMaterial3Dy = Medium(kgrid3Dy,materials); + mediumMaterial3Dy.materialIndexGrid = uint8(0); % solver3DyA = AcousticSolver(kgrid3Dy, medium3Dy, source3Dy, [], settings); solver3DyB = AcousticSolver(kgrid3Dy, mediumMaterial3Dy, source3Dy, [], settings); @@ -720,20 +743,18 @@ function testMediumAgreement(testCase) solver3DyB.run(Nt=Nt, dt=dt); testCase.verifyThat(solver3DyA.pressure, IsEqualTo(solver3DyB.pressure, "Within", tol)); % - kgrid3Dz = Grid([Nlat,Nlat,Nax], dx, [0,0,pmlSize]); + kgrid3Dz = Grid([Nlat,Nlat,Nax], dx, [0,0,pmlSize]); source3Dz = AcousticSource(kgrid3Dz); source3Dz.initialPressure = repmat(reshape(source1D.initialPressure, 1, 1, []), [Nlat, Nlat, 1]); % Construct Acoustic Medium medium3Dz = AcousticMedium(kgrid3Dz); medium3Dz.soundSpeed = c0*ones(kgrid3Dz.gridSize); - medium3Dz.soundSpeed(:,:,ceil(Nax/2):end) = c0/0.9; - medium3Dz.density = rho0; - medium3Dz.absorptionPower=alphay; - medium3Dz.absorptionCoeff=alpha0; + medium3Dz.density = rho0; + medium3Dz.absorptionPower = alphay; + medium3Dz.absorptionCoeff = alpha0; % Construct Medium - mediumMaterial3Dz=Medium(kgrid3Dz); - mediumMaterial3Dz.materialIDGrid = ones(kgrid3Dz.gridSize); - mediumMaterial3Dz.materialIDGrid(:,:,ceil(Nax/2):end) = 2; + mediumMaterial3Dz = Medium(kgrid3Dz, materials); + mediumMaterial3Dz.materialIndexGrid = uint8(0); % solver3DzA = AcousticSolver(kgrid3Dz, medium3Dz, source3Dz, [], settings); solver3DzB = AcousticSolver(kgrid3Dz, mediumMaterial3Dz, source3Dz, [], settings); @@ -743,39 +764,41 @@ function testMediumAgreement(testCase) % end + %Test the case in which an initial velocity and pressure are + %defined. function TestInitialVelocityAnalytic(testCase) import kwave.toolbox.* import matlab.unittest.constraints.IsEqualTo tol = matlab.unittest.constraints.AbsoluteTolerance(single(1e-6)); - Nax = 256; - dx = 4e-3; - c0 = 1500; - rho0=1000; + Nax = 256; + dx = 4e-3; + c0 = 1500; + rho0 = 1000; pmlSize = 20; - CFL = 0.25; - Nt = 150; - dt = CFL * dx / c0; + CFL = 0.25; + Nt = 150; + dt = CFL * dx / c0; + + kgrid = Grid(Nax, dx, pmlSize); - kgrid = Grid(Nax, dx, pmlSize); medium = AcousticMedium(kgrid); - c0 = 1500; medium.soundSpeed = c0; - medium.density = 1000; - source = AcousticSource(kgrid); - + medium.density = rho0; + source = AcousticSource(kgrid); v0 = @(x) exp( -(x.^2) ./ (5 * kgrid.dx).^2 )/(c0*rho0); - p0= @(x) exp( -(x.^2) ./ (5 * kgrid.dx).^2 ); - + p0 = @(x) exp( -(x.^2) ./ (5 * kgrid.dx).^2 ); source.initialVelocity = v0(kgrid.x); source.initialPressure = p0(kgrid.x); + settings = Settings; settings.plotSimulation = 'off'; - solver = AcousticSolver(kgrid, medium, source, [], settings); + solver = AcousticSolver(kgrid, medium, source, [], settings); solver.run(Nt=Nt, dt=dt); - FinTime=Nt*dt; + + FinTime = Nt*dt; % waves travel at speed of sound, and account for grid shift % from velocity to pressure. PressureActual = single(( p0(kgrid.x - c0*FinTime) + c0*rho0*v0(kgrid.x - c0*FinTime) + p0(kgrid.x + c0*FinTime) - c0*rho0*v0(kgrid.x + c0*FinTime) ))/2 ; @@ -783,7 +806,10 @@ function TestInitialVelocityAnalytic(testCase) end - function testVelcityPlaneWaves(testCase) + + % Test that plane waves are the same in all dimensions when the + % input is a velocity input + function testVelocityPlaneWaves(testCase) import kwave.toolbox.* import matlab.unittest.constraints.IsEqualTo @@ -792,61 +818,60 @@ function testVelcityPlaneWaves(testCase) tol = matlab.unittest.constraints.AbsoluteTolerance(single(1e-6)); % Test properties. - Nax = 256; - Nlat = 16; - dx = 4e-3; - c0 = 1500; - rho0 = 1000; - alpha0=10; + Nax = 256; + Nlat = 16; + dx = 4e-3; + c0 = 1500; + rho0 = 1000; + alpha0 = 10; pmlSize = 20; - CFL = 0.5; - Nt = 150; - dt = CFL * dx / c0; + CFL = 0.5; + Nt = 150; + dt = CFL * dx / c0; % Settings. settings = Settings; settings.plotSimulation = 'off'; % Construct Mediums - - kgrid1D = Grid(Nax, dx, pmlSize); + kgrid1D = Grid(Nax, dx, pmlSize); medium1D = AcousticMedium(kgrid1D); source1D = AcousticSource(kgrid1D); - source1D.initialPressure=0; + source1D.initialPressure = 0; source1D.initialVelocity = (1/(rho0*c0)) * exp( -(kgrid1D.xVec - 25e-3).^2 ./ (5 * kgrid1D.dx).^2 ); - kgrid2Dx = Grid([Nax, Nlat], dx, [pmlSize, 0]); + kgrid2Dx = Grid([Nax, Nlat], dx, [pmlSize, 0]); medium2Dx = AcousticMedium(kgrid2Dx); source2Dx = AcousticSource(kgrid2Dx); source2Dx.initialVelocity = zeros([Nax,Nlat,1,2]); - source2Dx.initialPressure=0; + source2Dx.initialPressure = 0; source2Dx.initialVelocity(:,:,:,1) = repmat(source1D.initialVelocity, [1, Nlat]); - kgrid2Dy = Grid([Nlat, Nax], dx, [0, pmlSize]); + kgrid2Dy = Grid([Nlat, Nax], dx, [0, pmlSize]); medium2Dy = AcousticMedium(kgrid2Dy); source2Dy = AcousticSource(kgrid2Dy); source2Dy.initialVelocity = zeros([Nlat,Nax,1,2]); - source2Dy.initialPressure=0; + source2Dy.initialPressure = 0; source2Dy.initialVelocity(:,:,:,2) = repmat(reshape(source1D.initialVelocity, 1, []), [Nlat, 1]); - kgrid3Dx = Grid([Nax, Nlat, Nlat], dx, [pmlSize, 0, 0]); + kgrid3Dx = Grid([Nax, Nlat, Nlat], dx, [pmlSize, 0, 0]); medium3Dx = AcousticMedium(kgrid3Dx); source3Dx = AcousticSource(kgrid3Dx); source3Dx.initialVelocity = zeros([Nax,Nlat,Nlat,3]); - source3Dx.initialPressure=0; + source3Dx.initialPressure = 0; source3Dx.initialVelocity(:,:,:,1) = repmat(source1D.initialVelocity, [1, Nlat, Nlat]); - kgrid3Dy = Grid([Nlat, Nax, Nlat], dx, [0,pmlSize, 0]); + kgrid3Dy = Grid([Nlat, Nax, Nlat], dx, [0,pmlSize, 0]); medium3Dy = AcousticMedium(kgrid3Dy); source3Dy = AcousticSource(kgrid3Dy); - source3Dy.initialPressure=0; + source3Dy.initialPressure = 0; source3Dy.initialVelocity = zeros([Nlat,Nax,Nlat,3]); source3Dy.initialVelocity(:,:,:,2) = repmat(reshape(source1D.initialVelocity, 1, [], 1), [Nlat, 1, Nlat]); - kgrid3Dz = Grid([Nlat, Nlat, Nax], dx, [0, 0, pmlSize]); + kgrid3Dz = Grid([Nlat, Nlat, Nax], dx, [0, 0, pmlSize]); medium3Dz = AcousticMedium(kgrid3Dz); source3Dz = AcousticSource(kgrid3Dz); - source3Dz.initialPressure=0; + source3Dz.initialPressure = 0; source3Dz.initialVelocity = zeros([Nlat,Nlat,Nax,3]); source3Dz.initialVelocity(:,:,:,3) = repmat(reshape(source1D.initialVelocity, 1, 1, []), [Nlat, Nlat, 1]); absorptionType='off'; @@ -871,75 +896,257 @@ function testVelcityPlaneWaves(testCase) medium3Dz.density = rho0; medium3Dz.absorptionCoeff= alpha0; - % Running Simulations - %Reference Solution solver1D = AcousticSolver(kgrid1D, medium1D, source1D, [], settings); solver1D.absorptionType=absorptionType; solver1D.run(Nt=Nt, dt=dt); pressure1D = solver1D.pressure; - density1D = solver1D.densitySplit; + density1D = solver1D.densitySplit; velocity1D = solver1D.velocity; solver2Dx = AcousticSolver(kgrid2Dx, medium2Dx, source2Dx, [], settings); solver2Dx.absorptionType=absorptionType; solver2Dx.run(Nt=Nt, dt=dt); pressure2Dx = squeeze(solver2Dx.pressure(:, end/2)); - density2Dx = squeeze(solver2Dx.densitySplit(:, end/2, 1, 1)); + density2Dx = squeeze(solver2Dx.densitySplit(:, end/2, 1, 1)); velocity2Dx = squeeze(solver2Dx.velocity(:, end/2, 1, 1)); solver2Dy = AcousticSolver(kgrid2Dy, medium2Dy, source2Dy, [], settings); solver2Dy.absorptionType=absorptionType; solver2Dy.run(Nt=Nt, dt=dt); pressure2Dy = reshape(squeeze(solver2Dy.pressure(end/2, :)), [], 1); - density2Dy = reshape(squeeze(solver2Dy.densitySplit(end/2, :, 1, 2)), [], 1); + density2Dy = reshape(squeeze(solver2Dy.densitySplit(end/2, :, 1, 2)), [], 1); velocity2Dy = reshape(squeeze(solver2Dy.velocity(end/2, :, 1, 2)), [], 1); - solver3Dx = AcousticSolver(kgrid3Dx, medium3Dx, source3Dx, [], settings); % Failed 1-5 + solver3Dx = AcousticSolver(kgrid3Dx, medium3Dx, source3Dx, [], settings); solver3Dx.absorptionType=absorptionType; solver3Dx.run(Nt=Nt, dt=dt); pressure3Dx = squeeze(solver3Dx.pressure(:, end/2, end/2)); - density3Dx = squeeze(solver3Dx.densitySplit(:, end/2, end/2, 1)); + density3Dx = squeeze(solver3Dx.densitySplit(:, end/2, end/2, 1)); velocity3Dx = squeeze(solver3Dx.velocity(:, end/2, end/2, 1)); solver3Dy = AcousticSolver(kgrid3Dy, medium3Dy, source3Dy, [], settings); solver3Dy.absorptionType=absorptionType; solver3Dy.run(Nt=Nt, dt=dt); pressure3Dy = reshape(squeeze(solver3Dy.pressure(end/2, :, end/2)), [], 1, 1); - density3Dy = reshape(squeeze(solver3Dy.densitySplit(end/2, :, end/2, 2)), [], 1, 1); + density3Dy = reshape(squeeze(solver3Dy.densitySplit(end/2, :, end/2, 2)), [], 1, 1); velocity3Dy = reshape(squeeze(solver3Dy.velocity(end/2, :, end/2, 2)), [], 1, 1); solver3Dz = AcousticSolver(kgrid3Dz, medium3Dz, source3Dz, [], settings); solver3Dz.absorptionType=absorptionType; solver3Dz.run(Nt=Nt, dt=dt); pressure3Dz = reshape(squeeze(solver3Dz.pressure(end/2, end/2, :)), [], 1, 1); - density3Dz = reshape(squeeze(solver3Dz.densitySplit(end/2, end/2, :, 3)), [], 1, 1); + density3Dz = reshape(squeeze(solver3Dz.densitySplit(end/2, end/2, :, 3)), [], 1, 1); velocity3Dz = reshape(squeeze(solver3Dz.velocity(end/2, end/2, :, 3)), [], 1, 1); % Testing - testCase.verifyThat(pressure2Dx, IsEqualTo(pressure1D, "Within", tol)); % - testCase.verifyThat(density2Dx, IsEqualTo(density1D, "Within", tol)); % - testCase.verifyThat(velocity2Dx, IsEqualTo(velocity1D, "Within", tol)); % + testCase.verifyThat(pressure2Dx, IsEqualTo(pressure1D, "Within", tol)); + testCase.verifyThat(density2Dx, IsEqualTo(density1D, "Within", tol)); + testCase.verifyThat(velocity2Dx, IsEqualTo(velocity1D, "Within", tol)); + + testCase.verifyThat(pressure2Dy, IsEqualTo(pressure1D, "Within", tol)); + testCase.verifyThat(density2Dy, IsEqualTo(density1D, "Within", tol)); + testCase.verifyThat(velocity2Dy, IsEqualTo(velocity1D, "Within", tol)); + + testCase.verifyThat(pressure3Dx, IsEqualTo(pressure1D, "Within", tol)); + testCase.verifyThat(density3Dx, IsEqualTo(density1D, "Within", tol)); + testCase.verifyThat(velocity3Dx, IsEqualTo(velocity1D, "Within", tol)); + + testCase.verifyThat(pressure3Dy, IsEqualTo(pressure1D, "Within", tol)); + testCase.verifyThat(density3Dy, IsEqualTo(density1D, "Within", tol)); + testCase.verifyThat(velocity3Dy, IsEqualTo(velocity1D, "Within", tol)); + + testCase.verifyThat(pressure3Dz, IsEqualTo(pressure1D, "Within", tol)); + testCase.verifyThat(density3Dz, IsEqualTo(density1D, "Within", tol)); + testCase.verifyThat(velocity3Dz, IsEqualTo(velocity1D, "Within", tol)); + + end + + %Test AcousticSolver automatically creates Nt and dt when CFL or + %EndTime are given but Nt and dt are not. + function TestAutomaticCreationOfTimeParameters(testCase) + + import kwave.toolbox.* + import matlab.unittest.constraints.IsEqualTo + toldt = matlab.unittest.constraints.AbsoluteTolerance(single(1e-6)); + tolNt = matlab.unittest.constraints.AbsoluteTolerance(single(1)); + + Nax = 128; + dx = 4e-3; + c0 = 1500; + rho0 = 1000; + alpha0 =10; + alphay =1.9; + + kgrid = Grid(Nax, dx); + medium = AcousticMedium(kgrid); + medium.soundSpeed = c0; + medium.density = rho0; + medium.absorptionPower = alphay; + medium.absorptionCoeff = alpha0; + + source = AcousticSource(kgrid); + p0 = @(x) exp( -(x.^2) ./ (5 * kgrid.dx).^2 ); + source.initialPressure = p0(kgrid.x); + + sensor = AcousticSensor(kgrid); + sensor.mask = ones(Nax,1); + sensor.pressureSensor = 'on'; + sensor.densitySensor = 'on'; + sensor.velocitySensor = 'ongrid'; + + settings = Settings; + settings.plotSimulation = 'off'; + + % Compute dt and Nt as in autoComputeTimeStep.m + CFL = 0.3; + endTime = Nax .* dx ./ c0; + dtTemp = CFL .* dx ./ c0; + NtExpected = ceil( endTime ./ dtTemp); + dtExpected = single(endTime / NtExpected); + + % Test when just CFL is defined + solver = AcousticSolver(kgrid, medium, source, sensor, settings); + solver.absorptionType='on'; + solver.run(CFL=CFL); + dt = solver.timeArray(2) - solver.timeArray(1); + Nt = length(solver.timeArray) - 1; % timeArray has length Nt+1 as it includes time 0 + + testCase.verifyThat(Nt, IsEqualTo(NtExpected, "Within", tolNt)); + testCase.verifyThat(dt, IsEqualTo(dtExpected, "Within", toldt)); + + % Test when just EndTime is defined + solver = AcousticSolver(kgrid, medium, source, [], settings); + solver.run(EndTime=endTime); + dt = solver.timeArray(2) - solver.timeArray(1); + Nt = length(solver.timeArray) - 1; % timeArray has length Nt+1 as it includes time 0; + + testCase.verifyThat(Nt, IsEqualTo(NtExpected, "Within", tolNt)); + testCase.verifyThat(dt, IsEqualTo(dtExpected, "Within", toldt)); + + % Test when both CFL and EndTime are defined + solver = AcousticSolver(kgrid, medium, source, [], settings); + solver.run(CFL=CFL,EndTime=endTime); + dt = solver.timeArray(2) - solver.timeArray(1); + Nt = length(solver.timeArray) - 1; % timeArray has length Nt+1 as it includes time 0; + + testCase.verifyThat(Nt, IsEqualTo(NtExpected, "Within", tolNt)); + testCase.verifyThat(dt, IsEqualTo(dtExpected, "Within", toldt)); + + end + + %Test sensorData is initialised when a sensor is defined + function TestInitialiseSensorData(testCase) + + import kwave.toolbox.* + import matlab.unittest.constraints.IsEqualTo + tol = matlab.unittest.constraints.AbsoluteTolerance(single(1e-6)); + + Nax = 32; + Nay = 32; + dx = 4e-3; + c0 = 1500; + rho0 = 1000; - testCase.verifyThat(pressure2Dy, IsEqualTo(pressure1D, "Within", tol)); % - testCase.verifyThat(density2Dy, IsEqualTo(density1D, "Within", tol)); % - testCase.verifyThat(velocity2Dy, IsEqualTo(velocity1D, "Within", tol)); % + kgrid = Grid([Nax Nay], dx); + medium = AcousticMedium(kgrid); + medium.soundSpeed = c0; + medium.density = rho0; + + source = AcousticSource(kgrid); + p0 = exp( -(kgrid.x.^2+kgrid.y.^2) ./ (5 * kgrid.dx).^2 ); + v0 = repmat(p0./ (c0*rho0), 1, 1, 1, 2); % dims [Nax Nay 1 2] + source.initialPressure = p0; + sensor = AcousticSensor(kgrid); + sensor.mask = ones(Nax, Nay); + sensor.pressureSensor = 'on'; + sensor.densitySensor = 'on'; + sensor.velocitySensor = 'on'; + + settings = Settings; + settings.plotSimulation = 'on'; - testCase.verifyThat(pressure3Dx, IsEqualTo(pressure1D, "Within", tol)); % - testCase.verifyThat(density3Dx, IsEqualTo(density1D, "Within", tol)); % - testCase.verifyThat(velocity3Dx, IsEqualTo(velocity1D, "Within", tol)); % + solver = AcousticSolver(kgrid, medium, source, sensor, settings); + solver.run(Nt=0,dt=1); - testCase.verifyThat(pressure3Dy, IsEqualTo(pressure1D, "Within", tol)); % - testCase.verifyThat(density3Dy, IsEqualTo(density1D, "Within", tol)); % - testCase.verifyThat(velocity3Dy, IsEqualTo(velocity1D, "Within", tol)); % + % Check the field variables everywhere at time 0 are the same + % as the initial conditions + testCase.verifyThat(solver.sensor.pressure, IsEqualTo(solver.pressure(:), "Within", tol)); + testCase.verifyThat(solver.sensor.velocity, IsEqualTo(reshape(solver.velocity,[Nax*Nay,2]), "Within", tol)); + testCase.verifyThat(solver.sensor.density, IsEqualTo(sum(reshape(solver.densitySplit,[Nax*Nay,2]),2), "Within", tol)); - testCase.verifyThat(pressure3Dz, IsEqualTo(pressure1D, "Within", tol)); % - testCase.verifyThat(density3Dz, IsEqualTo(density1D, "Within", tol)); % - testCase.verifyThat(velocity3Dz, IsEqualTo(velocity1D, "Within", tol)); % + solver.run(Nt=1,dt=1); % deliberately repeated to check some lines in executeTimeStep end + %Test that an error is thrown when the grids inside medium, source + %and sensor are not the same + function TestSameGrids(testCase) + + import kwave.toolbox.* + import matlab.unittest.constraints.IsEqualTo + + Nax = 128; + dx = 4e-3; + kgrid = Grid(Nax, dx); + kgrid2 = Grid(Nax+1, dx); % deliberately wrong grid size + medium = AcousticMedium(kgrid2); + source = AcousticSource(kgrid); + sensor = AcousticSensor(kgrid); + + % test for medium grid + fh = @() AcousticSolver(kgrid, medium, source, sensor); + testCase.verifyError(fh, "Solver:gridMismatch"); + + medium = AcousticMedium(kgrid); + source = AcousticSource(kgrid2); + sensor = AcousticSensor(kgrid); + + % test for source grid + fh = @() AcousticSolver(kgrid, medium, source, sensor); + testCase.verifyError(fh, "Solver:gridMismatch"); + + medium = AcousticMedium(kgrid); + source = AcousticSource(kgrid); + sensor = AcousticSensor(kgrid2); + + % test for sensor grid + fh = @() AcousticSolver(kgrid, medium, source, sensor); + testCase.verifyError(fh, "Solver:gridMismatch"); + + + end + + %Test that an error is thrown when medium is a GridInput object but + %not of the Medium or AcousticMedium class + function TestMediumNotCorrectClass(testCase) + + import kwave.toolbox.* + import matlab.unittest.constraints.IsEqualTo + + Nax = 128; + dx = 4e-3; + kgrid = Grid(Nax, dx); + source = AcousticSource(kgrid); + medium = source; % a GridInput object but not Medium or AcousticMedium class + sensor = AcousticSensor(kgrid); + + % test for medium being wrong type + fh = @() AcousticSolver(kgrid, medium, source, sensor); + testCase.verifyError(fh, "AcousticSolver:InvalidMediumType"); + + source = AcousticSource(kgrid); + medium = AcousticMedium(kgrid); + sensor = source; % not AcousticSensor class object + + % test for sensor being wrong type + fh = @() AcousticSolver(kgrid, medium, source, sensor); + testCase.verifyError(fh, "AcousticSolver:InvalidSensorType"); + + + end + + end end - diff --git a/+kwave/+tests/+unit/TestAcousticSolverGrid.m b/+kwave/+tests/+unit/TestAcousticSolverGrid.m index 0ffe77a8..da53f0fc 100644 --- a/+kwave/+tests/+unit/TestAcousticSolverGrid.m +++ b/+kwave/+tests/+unit/TestAcousticSolverGrid.m @@ -7,11 +7,28 @@ % %% Description % Runs the following tests for the AcousticSolver: +% % * Verifies that initial value problems in a homogeneous and lossless % medium match k-Wave-I. % * Verifies that if the initial velocity is specified as 0 the result is % the same as if it was not specified. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef TestAcousticSolverGrid < kwave.tests.unit.AbstractTestGrid % Parameterized tests. @@ -23,13 +40,13 @@ function initialValueProblemHomog(testCase) import kwave.toolbox.* import matlab.unittest.constraints.IsEqualTo - + % Medium. medium = AcousticMedium(testCase.kgrid); c0 = 1500; medium.soundSpeed = c0; medium.density = 1000; - + % Source. source = AcousticSource(testCase.kgrid); variance = (1 * testCase.kgrid.dx)^2; @@ -52,7 +69,7 @@ function initialValueProblemHomog(testCase) % Settings. settings = Settings; settings.plotSimulation = 'off'; - + % Solve. solver = AcousticSolver(testCase.kgrid, medium, source, [], settings); Nt = 25; @@ -145,7 +162,7 @@ function testInitialVelocitySteps(testCase) testCase.verifyThat(solver.velocity, IsEqualTo(solver1.velocity, "Within", testCase.tol)); testCase.verifyThat(solver2.pressure, IsEqualTo(solver1.pressure, "Within", testCase.tol)); testCase.verifyThat(solver2.velocity, IsEqualTo(solver1.velocity, "Within", testCase.tol)); - + medium.absorptionPower=1.9; medium.absorptionCoeff=0.5; solver2 = AcousticSolver(testCase.kgrid, medium, source2, [], settings); diff --git a/+kwave/+tests/+unit/TestAcousticSource.m b/+kwave/+tests/+unit/TestAcousticSource.m index d684b2c7..ef2c81ea 100644 --- a/+kwave/+tests/+unit/TestAcousticSource.m +++ b/+kwave/+tests/+unit/TestAcousticSource.m @@ -5,6 +5,22 @@ % Unit tests for the AcousticSource class using the AbstractTestGridInput % class. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef TestAcousticSource < kwave.tests.unit.AbstractTestGridInput properties @@ -17,7 +33,7 @@ end methods(Test) - + % Tests nested sub-references work, for example: % source.initialPressure.method.something(:, 1).other function testSubrefs(testCase) diff --git a/+kwave/+tests/+unit/TestComplexSource.m b/+kwave/+tests/+unit/TestComplexSource.m index 6bcdec76..bcb711bd 100644 --- a/+kwave/+tests/+unit/TestComplexSource.m +++ b/+kwave/+tests/+unit/TestComplexSource.m @@ -4,6 +4,22 @@ % % Unit tests for the ComplexSource class using the AbstractTestGridInput class. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef TestComplexSource < kwave.tests.unit.AbstractTestGridInput properties @@ -16,7 +32,7 @@ source testData end - + methods(TestMethodSetup) function setupTestData(testCase) testCase.source = kwave.toolbox.ComplexSource(testCase.kgrid); @@ -24,23 +40,23 @@ function setupTestData(testCase) testCase.source.sourceField = testCase.testData; end end - + methods(Test) - + function testSetAndGetSourceField(testCase) testCase.verifyEqual(testCase.source.sourceField, testCase.testData, 'AbsTol', 1e-6); end - + function testSourceFieldMagnitude(testCase) expectedMagnitude = abs(testCase.testData); testCase.verifyEqual(testCase.source.sourceFieldMagnitude, expectedMagnitude, 'AbsTol', 1e-6); end - + function testSourceFieldPhase(testCase) expectedPhase = angle(testCase.testData); testCase.verifyEqual(testCase.source.sourceFieldPhase, expectedPhase, 'AbsTol', 1e-6); end - + end end diff --git a/+kwave/+tests/+unit/TestElasticMedium.m b/+kwave/+tests/+unit/TestElasticMedium.m index 33aa62c0..7f37c1e4 100644 --- a/+kwave/+tests/+unit/TestElasticMedium.m +++ b/+kwave/+tests/+unit/TestElasticMedium.m @@ -4,6 +4,22 @@ % % Unit tests for the ElasticMedium class using the TestMedium class. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef TestElasticMedium < kwave.tests.unit.AbstractTestGridInput properties diff --git a/+kwave/+tests/+unit/TestElasticSource.m b/+kwave/+tests/+unit/TestElasticSource.m new file mode 100644 index 00000000..df6e438f --- /dev/null +++ b/+kwave/+tests/+unit/TestElasticSource.m @@ -0,0 +1,72 @@ +%% TestElasticSource +% *Package:* kwave.tests.unit +% *Superclasses:* kwave.tests.unit.AbstractTestGridInput +% +% Unit tests for the ElasticSource class using the AbstractTestGridInput +% class. + +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + +classdef TestElasticSource < kwave.tests.unit.AbstractTestGridInput + + properties + inputClass = 'ElasticSource' + inputProperties = {'initialPressure'} + inputPropertiesPadded = {'initialPressurePadded'} + inputPropertiesScalar = {} + inputPropertiesComplex = {} + inputPropertiesVectorField = {} + end + + methods(Test) + + % Tests nested sub-references work, for example: + % source.initialPressure.method.something(:, 1).other + function testSubrefs(testCase) + + % Initialize source object. + grid = kwave.toolbox.Grid([64,64,], [0.5,0.5]); + source = kwave.toolbox.ElasticSource(grid); + source.initialPressure = rand(source.gridSize); + + % Check slicing. + testCase.verifyEqual(size(source.initialPressure(1,:)), [1, 64]); + + end + + % Tests nested sub-assignments work, for example: + % source.initialPressure(idx) = val + function testSubasgn(testCase) + + % Initialize source object. + kgrid = kwave.toolbox.Grid([64,64,], [0.5,0.5]); + randValue = rand(kgrid.gridSize); + + source = kwave.toolbox.ElasticSource(kgrid); + source.initialPressure = randValue; + source.initialPressure(1:kgrid.Nx/4, :) = 2000; + + randValue(1:kgrid.Nx/4, :) = 2000; + + % Check assignment + testCase.verifyEqual(source.initialPressure, randValue); + + end + + end + +end diff --git a/+kwave/+tests/+unit/TestElectromagneticMedium.m b/+kwave/+tests/+unit/TestElectromagneticMedium.m deleted file mode 100644 index ccc73a59..00000000 --- a/+kwave/+tests/+unit/TestElectromagneticMedium.m +++ /dev/null @@ -1,21 +0,0 @@ -%% TestElectromagneticMedium -% *Package:* kwave.tests.unit -% *Superclasses:* kwave.tests.unit.AbstractTestGridInput -% -% Unit tests for the ElectromagneticMedium class using the TestMedium class. - -classdef TestElectromagneticMedium < kwave.tests.unit.AbstractTestGridInput - - properties - inputClass = 'ElectromagneticMedium' - inputProperties = {'permittivity', 'permeability', 'conductivity'} - inputPropertiesPadded = {'permittivityPadded', 'permeabilityPadded', 'conductivityPadded'} - inputPropertiesScalar = {} - inputPropertiesComplex = {} - inputPropertiesVectorField = {} - end - - methods(Test) - end - -end diff --git a/+kwave/+tests/+unit/TestElectromagneticSource.m b/+kwave/+tests/+unit/TestElectromagneticSource.m deleted file mode 100644 index 64a85f1c..00000000 --- a/+kwave/+tests/+unit/TestElectromagneticSource.m +++ /dev/null @@ -1,38 +0,0 @@ -%% TestElectromagneticSource -% *Package:* kwave.tests.unit -% *Superclasses:* kwave.tests.unit.AbstractTestGridInput -% -% Unit tests for the ElectromagneticSource class using the AbstractTestGridInput -% class. - -classdef TestElectromagneticSource < kwave.tests.unit.AbstractTestGridInput - - properties - inputClass = 'ElectromagneticSource' - inputProperties = {... - 'initialElectricField', ... - 'initialMagneticField', ... - 'electricCurrentDensitySource', ... - 'magneticCurrentDensitySource'} - inputPropertiesPadded = {... - 'initialElectricFieldPadded', ... - 'initialMagneticFieldPadded', ... - 'electricCurrentDensitySourcePadded', ... - 'magneticCurrentDensitySourcePadded'} - inputPropertiesScalar = {} - inputPropertiesComplex = {} - inputPropertiesVectorField = {... - 'initialElectricField', ... - 'initialMagneticField', ... - 'electricCurrentDensitySource', ... - 'magneticCurrentDensitySource', ... - 'initialElectricFieldPadded', .... - 'initialMagneticFieldPadded', ... - 'electricCurrentDensitySourcePadded', ... - 'magneticCurrentDensitySourcePadded'} - end - - methods(Test) - end - -end diff --git a/+kwave/+tests/+unit/TestExpandMatrix.m b/+kwave/+tests/+unit/TestExpandMatrix.m index 78c715f6..babb82df 100644 --- a/+kwave/+tests/+unit/TestExpandMatrix.m +++ b/+kwave/+tests/+unit/TestExpandMatrix.m @@ -4,6 +4,22 @@ % % Unit tests for the expandMatrix function. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef TestExpandMatrix < matlab.unittest.TestCase properties(TestParameter) @@ -18,9 +34,9 @@ % Parameterized tests. methods(Test, ParameterCombination="sequential") - % Expand using edge values. + % Expand using edge values. function testOutputSize(testCase, gridSize, expansionSize, expectedSize, expectedSum, contractionIndices) - + import kwave.toolbox.*; % Create matrix with integers and expand using edge values. @@ -43,9 +59,9 @@ function testOutputSize(testCase, gridSize, expansionSize, expectedSize, expecte end - % Expand using specified value. + % Expand using specified value. function testExpansionValue(testCase, gridSize, expansionSize, expectedZeros, contractionIndices) - + import kwave.toolbox.*; % Create matrix with integers and expand using zeros. @@ -68,7 +84,7 @@ function testExpansionValue(testCase, gridSize, expansionSize, expectedZeros, co % Test expansion of logical matrices. function testLogicalMatrix(testCase, gridSize, expansionSize, expectedZeros) import kwave.toolbox.*; - + matrix = true(gridSize); expandedMatrix = expandMatrix(matrix, expansionSize, false); diff --git a/+kwave/+tests/+unit/TestFourierCollocation.m b/+kwave/+tests/+unit/TestFourierCollocation.m index 6a9ba65d..300fd69b 100644 --- a/+kwave/+tests/+unit/TestFourierCollocation.m +++ b/+kwave/+tests/+unit/TestFourierCollocation.m @@ -10,6 +10,22 @@ % functions that are periodic on the test grid. All dimensions and grid % staggering options are tested. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef TestFourierCollocation < kwave.tests.unit.AbstractTestGrid % Add solver to class properties so we can pass it to each testCase. @@ -92,7 +108,7 @@ function testCurl(testCase) import matlab.unittest.constraints.IsEqualTo switch testCase.kgridPadded.dimensions - case 1 + case 1 % Test to check incorrect number of dimensions gives exception f = rand(testCase.kgridPadded.Nx,1); @@ -105,7 +121,7 @@ function testCurl(testCase) testCase.verifyError(@() testCase.solver.curl(f), 'FourierCollocation:not3DVectorField'); case 3 - + % No staggering. [f, testCase.referenceSolution] = testCase.getPeriodicVectorFunctionCurl; testCase.actualSolution = testCase.solver.curl(f); @@ -115,12 +131,12 @@ function testCurl(testCase) [f, testCase.referenceSolution] = testCase.getPeriodicVectorFunctionCurl("forward"); testCase.actualSolution = testCase.solver.curl(f, Staggering="forward"); testCase.verifyThat(testCase.actualSolution, IsEqualTo(testCase.referenceSolution, "Within", testCase.tol)); - + % Backward staggering. [f, testCase.referenceSolution] = testCase.getPeriodicVectorFunctionCurl("backward"); testCase.actualSolution = testCase.solver.curl(f, Staggering="backward"); testCase.verifyThat(testCase.actualSolution, IsEqualTo(testCase.referenceSolution, "Within", testCase.tol)); - + % Scalar kappa. testCase.solver.kappa = 2; [f, testCase.referenceSolution] = testCase.getPeriodicVectorFunctionCurl; @@ -143,11 +159,11 @@ function testDivCurl(testCase) % For this test, the tolerance was changed to 1e-5, instead of % the default 1e-6, because round-off error accumulates quickly - % when taking Fourier derivatives. The error is large because + % when taking Fourier derivatives. The error is large because % ddxNoShift etc are defined in the FourierCollocation class % as single precision. temporaryTolerance = matlab.unittest.constraints.AbsoluteTolerance(single(1e-5)); - + if testCase.kgridPadded.dimensions == 3 % No staggering. @@ -155,7 +171,7 @@ function testDivCurl(testCase) testCase.referenceSolution = zeros(testCase.kgridPadded.gridSize); testCase.actualSolution = testCase.solver.divergence(testCase.solver.curl(f)); testCase.verifyThat(testCase.actualSolution, IsEqualTo(testCase.referenceSolution, "Within", temporaryTolerance)); - + % Forward staggering. [f, ~] = testCase.getPeriodicVectorFunctionCurl("forward"); testCase.referenceSolution = zeros(testCase.kgridPadded.gridSize); @@ -167,7 +183,7 @@ function testDivCurl(testCase) testCase.referenceSolution = zeros(testCase.kgridPadded.gridSize); testCase.actualSolution = testCase.solver.divergence(testCase.solver.curl(f, Staggering="backward"), Staggering="backward"); testCase.verifyThat(testCase.actualSolution, IsEqualTo(testCase.referenceSolution, "Within", temporaryTolerance)); - + end end @@ -179,11 +195,11 @@ function testCurlGrad(testCase) % For this test, the tolerance was changed to 1e-3, instead of % the default 1e-6, because round-off error accumulates quickly - % when taking Fourier derivatives. The error is large because + % when taking Fourier derivatives. The error is large because % ddxNoShift etc are defined in the FourierCollocation class % as single precision. temporaryTolerance = matlab.unittest.constraints.AbsoluteTolerance(single(1e-3)); - + if testCase.kgridPadded.dimensions == 3 % No staggering. @@ -236,7 +252,7 @@ function testLaplacian(testCase) [f, testCase.referenceSolution] = testCase.getPeriodicVectorFunctionLap("backward"); testCase.actualSolution = -testCase.solver.fracLaplacian(f,1, Staggering="backward"); testCase.verifyThat(testCase.actualSolution, IsEqualTo(testCase.referenceSolution, "Within", testCase.tol)); - + end function testStagger(testCase) @@ -282,9 +298,9 @@ function testStagger(testCase) testCase.actualSolution=testCase.actualSolution(2:end-1,2:end-1,2:end-1,:); end testCase.verifyThat(testCase.actualSolution, IsEqualTo(testCase.referenceSolution, "Within", testCase.tol)); - + end - + end % Test utilities. @@ -327,9 +343,9 @@ function testStagger(testCase) [X, Y] = ndgrid(obj.kgridPadded.xVec, obj.kgridPadded.yVec); [Xsg, Ysg] = ndgrid(xSg, ySg); - + F = sin(kx .* X) .* sin(ky .* Y) ./ kx; - + gradF = zeros([size(F), 1, 2]); gradF(:, :, :, 1) = cos(kx .* Xsg) .* sin(ky .* Y); gradF(:, :, :, 2) = sin(kx .* X) .* cos(ky .* Ysg) .* (ky ./ kx); @@ -337,12 +353,12 @@ function testStagger(testCase) kx = (2*pi ./ obj.kgridPadded.xSize); ky = (2*pi ./ obj.kgridPadded.ySize); kz = (2*pi ./ obj.kgridPadded.zSize); - + [X, Y, Z] = ndgrid(obj.kgridPadded.xVec, obj.kgridPadded.yVec, obj.kgridPadded.zVec); [Xsg, Ysg, Zsg] = ndgrid(xSg, ySg, zSg); - + F = sin(kx .* X) .* sin(ky .* Y) .* sin(kz .* Z) ./ kx; - + gradF = zeros([size(F), 3]); gradF(:, :, :, 1) = cos(kx .* Xsg) .* sin(ky .* Y) .* sin(kz .* Z); gradF(:, :, :, 2) = sin(kx .* X) .* cos(ky .* Ysg) .* sin(kz .* Z) .* (ky ./ kx); @@ -384,40 +400,40 @@ function testStagger(testCase) case 2 kx = (2*pi ./ obj.kgridPadded.xSize); ky = (2*pi ./ obj.kgridPadded.ySize); - + [X, Y] = ndgrid(obj.kgridPadded.xVec, obj.kgridPadded.yVec); [Xsg, Ysg] = ndgrid(xSg, ySg); - + Fx = sin(kx .* X) ./ kx; Fy = sin(ky .* Y) ./ ky; - + F = cat(4, Fx, Fy); - + divF = cos(kx .* Xsg) + cos(ky .* Ysg); case 3 kx = (2*pi ./ obj.kgridPadded.xSize); ky = (2*pi ./ obj.kgridPadded.ySize); kz = (2*pi ./ obj.kgridPadded.zSize); - + [X, Y, Z] = ndgrid(obj.kgridPadded.xVec, obj.kgridPadded.yVec, obj.kgridPadded.zVec); [Xsg, Ysg, Zsg] = ndgrid(xSg, ySg, zSg); - + Fx = sin(kx .* X) ./ kx; Fy = sin(ky .* Y) ./ ky; Fz = sin(kz .* Z) ./ kz; - + F = cat(4, Fx, Fy, Fz); - + divF = cos(kx .* Xsg) + cos(ky .* Ysg) + cos(kz .* Zsg); end end - + % Define a periodic scalar function and its analytic laplacian on % the grid specified by obj.kgridPadded. The function is normalised % so the maximum of the gradient is approximately 1. The Laplacian % can also be returned on a staggered grid. function [F, LapF] = getPeriodicVectorFunctionLap(obj, staggering) - + arguments obj staggering(1,:) char {mustBeMember(staggering, {'none', 'forward', 'backward'})} = 'none' @@ -436,7 +452,7 @@ function testStagger(testCase) ySg = obj.kgridPadded.yVec - obj.kgridPadded.dy/2; zSg = obj.kgridPadded.zVec - obj.kgridPadded.dz/2; end - + switch obj.kgridPadded.dimensions case 1 kx = (2*pi ./ obj.kgridPadded.xSize); @@ -449,7 +465,7 @@ function testStagger(testCase) [Xsg, Ysg] = ndgrid(xSg, ySg); Fx = sin(kx .* X) ./ (kx); Fy = sin(ky .* Y) ./ (ky); - F = Fx.*Fy; + F = Fx.*Fy; Fx = sin(kx .* Xsg) ./ (kx); Fy = sin(ky .* Ysg) ./ (ky); LapF = -(kx^2+ky.^2).*Fx.*Fy; @@ -469,8 +485,8 @@ function testStagger(testCase) LapF = -(kx^2+ky.^2+kz.^2).*Fx.*Fy.*Fz; end end - - % Define a periodic scalar function on the grid specified by obj.kgridPadded. + + % Define a periodic scalar function on the grid specified by obj.kgridPadded. % The function is normalised and staggered, suitable for returning stagger % by Fourier Methods. function [F,FStg]= getPeriodicVectorFunctionStg(obj, staggering) @@ -525,7 +541,7 @@ function testStagger(testCase) end end - % Define a linear scalar function on the grid specified by obj.kgridPadded. + % Define a linear scalar function on the grid specified by obj.kgridPadded. % The function is exact for linear interpolation to perform grid staggering. function [F,FStg]= getLinearVectorFunctionStg(obj, staggering) arguments @@ -579,7 +595,7 @@ function testStagger(testCase) FStg(:,:,:,3)=(Fx+Fy+FSgz)/3; end end - + % Define a periodic vector function and its analytic curl on % the grid specified by obj.kgridPadded. The function is normalized so % the maximum of the curl is approximately 1. The curl @@ -604,20 +620,20 @@ function testStagger(testCase) ySg = obj.kgridPadded.yVec - obj.kgridPadded.dy/2; zSg = obj.kgridPadded.zVec - obj.kgridPadded.dz/2; end - + % Only need the 3D case here as curl is only defined for the 3D case % Choose wavenumbers to make function periodic on padded grid kx = (2*pi ./ obj.kgridPadded.xSize); ky = (2*pi ./ obj.kgridPadded.ySize); kz = (2*pi ./ obj.kgridPadded.zSize); - + [X, Y, Z] = ndgrid(obj.kgridPadded.xVec, obj.kgridPadded.yVec, obj.kgridPadded.zVec); [Xsg, Ysg, Zsg] = ndgrid(xSg, ySg, zSg); % Define a periodic vector function (on a staggered Yee cell) - F(:,:,:,1) = ( sin(ky .* Y)/ky ) .* ( sin(kz .* Z)/kz ); - F(:,:,:,2) = ( sin(kx .* X)/kx ) .* ( sin(kz .* Z)/kz ); - F(:,:,:,3) = ( sin(kx .* X)/kx ) .* ( sin(ky .* Y)/ky ); + F(:,:,:,1) = ( sin(ky .* Y)/ky ) .* ( sin(kz .* Z)/kz ); + F(:,:,:,2) = ( sin(kx .* X)/kx ) .* ( sin(kz .* Z)/kz ); + F(:,:,:,3) = ( sin(kx .* X)/kx ) .* ( sin(ky .* Y)/ky ); % Calculate the components of the analytical curl of F dFxdy = cos(ky .* Ysg) .* ( sin(kz .* Z )/kz ); @@ -629,7 +645,7 @@ function testStagger(testCase) % Construct the analytical curl of F curlF(:,:,:,1) = dFzdy - dFydz; - curlF(:,:,:,2) = dFxdz - dFzdx; + curlF(:,:,:,2) = dFxdz - dFzdx; curlF(:,:,:,3) = dFydx - dFxdy; end diff --git a/+kwave/+tests/+unit/TestGrid.m b/+kwave/+tests/+unit/TestGrid.m index 505a9ce4..9a3a97e4 100644 --- a/+kwave/+tests/+unit/TestGrid.m +++ b/+kwave/+tests/+unit/TestGrid.m @@ -4,6 +4,22 @@ % % Unit tests for the kwave.toolbox.Grid class. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef TestGrid < matlab.unittest.TestCase properties @@ -54,7 +70,7 @@ methods(TestMethodSetup, ParameterCombination="sequential") % Create Grid object used by tests. - function createSource(testCase, gridSize, gridSpacing, gridSizePadded, gridSpacingPadded, primeFactors) %#ok + function createSource(testCase, gridSize, gridSpacing, gridSizePadded, gridSpacingPadded, primeFactors) %#ok import kwave.toolbox.*; testCase.kgrid = Grid(gridSize, gridSpacing); end @@ -63,8 +79,8 @@ function createSource(testCase, gridSize, gridSpacing, gridSizePadded, gridSpaci % Parameterized tests. methods(Test, ParameterCombination="sequential") - - % Test grid size properties are correct. + + % Test grid size properties are correct. function testGridSize(testCase, gridSizePadded, gridSpacingPadded) % Check grid size and spacing. diff --git a/+kwave/+tests/+unit/TestLogger.m b/+kwave/+tests/+unit/TestLogger.m index 1a57f5e2..86e1f1d1 100644 --- a/+kwave/+tests/+unit/TestLogger.m +++ b/+kwave/+tests/+unit/TestLogger.m @@ -4,12 +4,28 @@ % % Unit tests for the kwave.toolbox.Logger class. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef TestLogger < matlab.unittest.TestCase - + properties logFileName = 'testLogFile.txt'; end - + methods (TestClassSetup) % Create an empty log file. @@ -19,34 +35,34 @@ function createLogFile(testCase) end end - + methods (TestClassTeardown) % Delete the log file. function deleteLogFile(testCase) kwave.toolbox.Logger.reset; delete(testCase.logFileName); - end + end end - + methods (Test) - + % Smoke test for the debug log method. function testLoggerDebugMethod(~) kwave.toolbox.Logger.debug('Debug message.'); end - + % Smoke test for the info log method. function testLoggerInfoMethod(~) kwave.toolbox.Logger.info('Information message.'); end - + % Smoke test for the warning log method. function testLoggerWarningMethod(~) kwave.toolbox.Logger.warning('Warning message.'); end - + % Test the error log method. function testLoggerErrorMethod(testCase) testCase.verifyError(@() kwave.toolbox.Logger.error('TestClass:CustomErrorID', 'Error message.'), 'TestClass:CustomErrorID'); @@ -57,10 +73,10 @@ function testSetLogToFile(testCase) kwave.toolbox.Logger.setLogToFile(testCase.logFileName); message = 'Message written to a file.'; kwave.toolbox.Logger.info(message); - + fileContent = fileread(testCase.logFileName); testCase.verifyNotEmpty(strfind(fileContent, message)); - end + end % Test setting the log level and verify the line is not added. function testSetLogLevel(testCase) @@ -68,7 +84,7 @@ function testSetLogLevel(testCase) kwave.toolbox.Logger.setLogToFile(testCase.logFileName); message = 'This message should not appear.'; kwave.toolbox.Logger.debug(message); - + fileContent = fileread(testCase.logFileName); testCase.verifyEmpty(strfind(fileContent, message)); end @@ -90,7 +106,7 @@ function testSetLogToCommandLine(~) kwave.toolbox.Logger.setLogToCommandLine(); kwave.toolbox.Logger.info('Message written to command line.'); end - + end - + end diff --git a/+kwave/+tests/+unit/TestMaterials.m b/+kwave/+tests/+unit/TestMaterials.m new file mode 100644 index 00000000..eff2ee92 --- /dev/null +++ b/+kwave/+tests/+unit/TestMaterials.m @@ -0,0 +1,204 @@ +%% TestMaterials +% *Package:* kwave.tests.unit +% *Superclasses:* matlab.unittest.TestCase +% +% Unit tests for the kwave.toolbox.Materials class. + +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + +classdef TestMaterials < matlab.unittest.TestCase + + properties + materials + end + + + methods (TestMethodSetup) + function createMaterials(testCase) + testCase.materials = kwave.toolbox.Materials(); + end + end + + + methods (Test) + + function testMaterialName(testCase) + + % Try to name a material with a number + s = struct('soundSpeed', 1540, 'density', 1000); + name = 0; % shouldn't be a number + testCase.verifyError( ... + @() testCase.materials.addMaterial(name, s), ... + 'Materials:InvalidNameType'); + + end + + function testDefaultMaterialsExist(testCase) + + % Defaults: water + air should exist + T = testCase.materials.listMaterialIndices(); + testCase.assertNotEmpty(T.Name == "water", ... + '"water" should be one of the default materials.'); + testCase.assertNotEmpty(T.Name == "air", ... + '"air" should be one of the default materials.'); + + % Listing must be sorted ascending by index + testCase.verifyEqual(T.Index, sort(T.Index), ... + 'Material indices should always be sorted in ascending order.'); + testCase.verifyClass(T.Index, 'uint8'); + + end + + function testAutoAssignIndex(testCase) + + % Compute the next free (lowest) index in [0..255] from the object + T0 = testCase.materials.listMaterialIndices(); + used = T0.Index; + free = setdiff(uint8(0:255), used, 'stable'); + testCase.assumeFalse(isempty(free), 'No free indices available to test auto-assignment.'); + + s = struct('soundSpeed', 1500, 'density', 1000); + idx = testCase.materials.addMaterial('tissue', s); + + % Returns a uint8 and equals the lowest free index we computed + testCase.assertClass(idx, 'uint8'); + testCase.verifyEqual(idx, uint8(free(1))); + + % Check it's in the listing + T = testCase.materials.listMaterialIndices(); + testCase.assertNotEmpty(T.Name == "tissue", ... + '"tissue" material was not found after insertion.'); + + % Listing must be sorted ascending by index + testCase.verifyEqual(T.Index, sort(T.Index), ... + 'Material indices should always be sorted in ascending order.'); + + end + + function testExplicitIndexAcceptedAndSorting(testCase) + + % Add with explicit indices (out of order to test sort) + testCase.materials.addMaterial('mA', struct('index', 50, 'soundSpeed',1500,'density',1000)); + testCase.materials.addMaterial('mB', struct('index', 5, 'soundSpeed',1400,'density', 950)); + idx = testCase.materials.addMaterial('mC', struct('index', 200, 'soundSpeed',1600,'density',1100)); + testCase.assertEqual(idx, uint8(200)); + + % Listing must be sorted ascending by index + inds = testCase.materials.listMaterialIndices().Index; + testCase.verifyEqual(inds, sort(inds), ... + 'Material indices should always be sorted in ascending order.'); + + + end + + function testDuplicateNameError(testCase) + + % add a material + s = struct('soundSpeed', 1500, 'density', 1000); + testCase.materials.addMaterial('myMat', s); + + % Try to add another material with the same name + testCase.verifyError( ... + @() testCase.materials.addMaterial('myMat', s), ... + 'Materials:NameExists', ... + 'Adding a material with an existing name should trigger a NameExists error.'); + end + + function testDuplicateExplicitIndexError(testCase) + + % Pick an explicit free index safely + used = testCase.materials.listMaterialIndices().Index; + free = setdiff(uint8(0:255), used, 'stable'); + testCase.assumeGreaterThanOrEqual(numel(free), 1); + I = free(1); + + % add a material with this index + s1 = struct('index', I, 'soundSpeed', 1500, 'density', 1000); + testCase.materials.addMaterial('m1', s1); + + % try to add another material with the same index + s2 = struct('index', I, 'soundSpeed', 1400, 'density', 900); + testCase.verifyError( ... + @() testCase.materials.addMaterial('m2', s2), ... + 'Materials:IndexInUse'); + end + + function testMissingRequiredFieldsError(testCase) + + % Missing density + s = struct('soundSpeed', 1500); + testCase.verifyError( ... + @() testCase.materials.addMaterial('bad', s), ... + 'Materials:MissingField'); + end + + function testExhaustionOfIndices(testCase) + + % Fill every remaining free index explicitly so that auto-assign has no space + used = testCase.materials.listMaterialIndices().Index; + free = setdiff(uint8(0:255), used, 'stable'); + + for ii = 1:numel(free) + name = sprintf('mat_%d', free(ii)); + s = struct('index', free(ii), 'soundSpeed', 1500, 'density', 1000); + testCase.materials.addMaterial(name, s); + end + + % Now all 256 indices should be used -> auto-assign should error + testCase.verifyError( ... + @() testCase.materials.addMaterial('extra', struct('soundSpeed',1500,'density',1000)), ... + 'Materials:NoIndicesLeft'); + end + + function testAddMaterials(testCase) + + % Index shouldn't be -1 + testCase.verifyError( ... + @() testCase.materials.addMaterial('testTissue', struct('index', -1,'soundSpeed',1500,'density',1000)), ... + 'Materials:InvalidIndexField'); + + % SoundSpeed shouldn't be positive + testCase.verifyError( ... + @() testCase.materials.addMaterial('testTissue', struct('soundSpeed',-1500,'density',1000)), ... + 'Materials:NotNonNegativeScalar'); + + % name shouldn't be a number + testCase.verifyError( ... + @() testCase.materials.addMaterial(0, struct('soundSpeed',1500,'density',1000)), ... + 'Materials:InvalidNameType'); + + % name shouldn't be in RESERVED_PROPS + testCase.verifyError( ... + @() testCase.materials.addMaterial('RESERVED_PROPS', struct('soundSpeed',1500,'density',1000)), ... + 'Materials:ReservedName'); + + % Can't have an unknown field + testCase.verifyError( ... + @() testCase.materials.addMaterial('testTissue', struct('soundSpeed',1500,'density',1000,'unknownField',1500)), ... + 'Materials:UnknownField'); + + end + + function testListMaterials(testCase) + + testCase.verifyWarningFree(@() testCase.materials.listMaterials()); + + end + + end + +end diff --git a/+kwave/+tests/+unit/TestMedium.m b/+kwave/+tests/+unit/TestMedium.m index 6e93c89a..42d073e2 100644 --- a/+kwave/+tests/+unit/TestMedium.m +++ b/+kwave/+tests/+unit/TestMedium.m @@ -1,46 +1,230 @@ %% TestMedium % *Package:* kwave.tests.unit -% *Superclasses:* kwave.tests.unit.AbstractTestGridInput +% *Superclasses:* matlab.unittest.TestCase % -% Unit tests for the Medium class using the TestMedium class. +% Unit tests for the kwave.toolbox.Medium class. + +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . classdef TestMedium < kwave.tests.unit.AbstractTestGridInput properties - inputClass = 'Medium' - inputProperties = {'materialIDGrid'} - inputPropertiesPadded = {'materialIDGridPadded'} - inputPropertiesScalar = {'soundSpeedReference', 'diffusionReference'} - inputPropertiesComplex = {} - inputPropertiesVectorField = {'materialTable'} + kgrid + materials + medium + airIdx + waterIdx + airC + waterC + airRho + waterRho + end + + methods (TestMethodSetup) + function makeFixtures(testCase) + + % Small 2D grid + testCase.kgrid = kwave.toolbox.Grid([6, 4], [1e-3, 1e-3]); + + % Materials with defaults ("water", "air") + testCase.materials = kwave.toolbox.Materials(); + + % Medium under test + testCase.medium = kwave.toolbox.Medium(testCase.kgrid, testCase.materials); + + % Shorthands + testCase.airIdx = testCase.materials.air.index; + testCase.waterIdx = testCase.materials.water.index; + testCase.airC = testCase.materials.air.soundSpeed; + testCase.waterC = testCase.materials.water.soundSpeed; + testCase.airRho = testCase.materials.air.density; + testCase.waterRho = testCase.materials.water.density; + + end end - %Unpadded methods call the padded methods - methods(Test, ParameterCombination="sequential") - - function callVariablesTest(testCase) - - import kwave.toolbox.* - - mediumConstant = Medium(testCase.kgrid); - mediumVaried = Medium(testCase.kgrid); - mediumConstant.materialIDGrid=1; - mediumVaried.materialIDGrid= ones(testCase.kgrid.gridSize); - mediumVaried.materialIDGrid(1)=2; - mediumVaried.BonA; - mediumVaried.density; - mediumVaried.soundSpeed; - mediumVaried.specificHeat; - mediumVaried.thermalConductivity; - mediumVaried.absorptionCoeff; - mediumVaried.absorptionPower; - mediumConstant.BonA; - mediumConstant.density; - mediumConstant.soundSpeed; - mediumConstant.specificHeat; - mediumConstant.thermalConductivity; - mediumConstant.absorptionCoeff; - mediumConstant.absorptionPower; + methods (Test) + + function setGet_materialIndexGrid(testCase) + + % Homogeneous water domain + idx = repmat(testCase.waterIdx, testCase.kgrid.Nx, testCase.kgrid.Ny); + testCase.medium.materialIndexGrid = idx; + + got = testCase.medium.materialIndexGrid; + testCase.assertClass(got, 'uint8', 'materialIndexGrid must always store uint8 indices.'); + testCase.assertSize(got, [testCase.kgrid.Nx, testCase.kgrid.Ny], ... + 'materialIndexGrid must preserve grid size.'); + testCase.verifyTrue(all(got(:) == testCase.waterIdx), ... + 'materialIndexGrid did not set/get the same index everywhere for a homogeneous water domain.'); + + end + + function sizeValidation_WrongSize_Errors(testCase) + + % Assign something with the wrong size and confirm it errors. + bad = zeros(testCase.kgrid.Nx+1, testCase.kgrid.Ny, 'uint8'); + didError = false; + try + testCase.medium.materialIndexGrid = bad; + catch + didError = true; + end + testCase.verifyTrue(didError, 'Assigning wrong-sized materialIndexGrid should error.'); + + end + + function tryToAssignReadOnly(testCase) + + % Assign to a read-only medium property and confirm is errors. + try + testCase.medium.soundSpeed = 1; + catch + didError = true; + end + testCase.verifyTrue(didError, 'Trying to assign to read-only medium properties should error.'); + + end + + function checkRefreshWorks(testCase) + passed = true; + try + testCase.medium = refresh(testCase.medium); + catch + passed = false; + end + testCase.verifyTrue(passed, 'refresh should not throw an error'); + + end + + function derivedMaps_Homogeneous(testCase) + + % Water, water everywhere + idx = repmat(testCase.waterIdx, testCase.kgrid.Nx, testCase.kgrid.Ny); + testCase.medium.materialIndexGrid = idx; + + c = testCase.medium.soundSpeed; + rho = testCase.medium.density; + + testCase.verifySize(c, [testCase.kgrid.Nx, testCase.kgrid.Ny]); + testCase.verifySize(rho, [testCase.kgrid.Nx, testCase.kgrid.Ny]); + + testCase.verifyTrue(all(c(:) == testCase.waterC)); + testCase.verifyTrue(all(rho(:) == testCase.waterRho)); + + end + + function derivedMaps_Heterogeneous_TopAir_BottomWater(testCase) + + % Build a 2-material field: top-half air, bottom-half water + idx = repmat(testCase.waterIdx, testCase.kgrid.Nx, testCase.kgrid.Ny); + idx(1:floor(end/2), :) = testCase.airIdx; + testCase.medium.materialIndexGrid = idx; + + c = testCase.medium.soundSpeed; + rho = testCase.medium.density; + + testCase.verifyTrue(all(c(1:end/2, :) == testCase.airC,'all')); + testCase.verifyTrue(all(c(end/2+1:end, :) == testCase.waterC,'all')); + testCase.verifyTrue(all(rho(1:end/2, :) == testCase.airRho,'all')); + testCase.verifyTrue(all(rho(end/2+1:end,:) == testCase.waterRho,'all')); + + end + + function subsasgn_SlicedAssignment_Path(testCase) + + % Exercise Medium's subsasgn path for its virtual property by + % assigning only the top row to air and leave rest as water. + idx = repmat(testCase.waterIdx, testCase.kgrid.Nx, testCase.kgrid.Ny); + testCase.medium.materialIndexGrid = idx; + + % This must go through Medium's subsasgn (virtual property). + testCase.medium.materialIndexGrid(1, :) = testCase.airIdx; + + % Verify mapping picked up for that slice only + c = testCase.medium.soundSpeed; + testCase.verifyTrue(all(c(1, :) == testCase.airC,'all')); + testCase.verifyTrue(all(c(2:end, :) == testCase.waterC,'all')); + + end + + function materialsHandle_IsImmutable(testCase) + + % Attempting to reassign .materials should be prohibited + didError = false; + try + testCase.medium.materials = testCase.materials; + catch ME + didError = true; + testCase.verifyEqual(ME.identifier, 'MATLAB:class:SetProhibited'); + end + testCase.verifyTrue(didError, 'Expected Medium.materials to be immutable.'); + + end + + function display_DoesNotError(testCase) + + % Test custom display / propertyGroups + disp(testCase.medium); + testCase.verifyTrue(true); + + end + + function derivativeMaps_SizeMatchesGrid_AfterReassignment(testCase) + + % Reassign a new checkerboard pattern and confirm size stays consistent + idx = repmat(testCase.waterIdx, testCase.kgrid.Nx, testCase.kgrid.Ny); + idx(1:2:end, :) = testCase.airIdx; % every second row is air + testCase.medium.materialIndexGrid = idx; + + c = testCase.medium.soundSpeed; + rho = testCase.medium.density; + + testCase.verifySize(c, [testCase.kgrid.Nx, testCase.kgrid.Ny]); + testCase.verifySize(rho, [testCase.kgrid.Nx, testCase.kgrid.Ny]); + + % Spot-check a few positions + testCase.verifyEqual(c(1,1), testCase.airC); + testCase.verifyEqual(c(2,1), testCase.waterC); + testCase.verifyEqual(rho(3,2), testCase.airRho); + + end + + function testGridIndexExpansion(testCase) + + % assigned properties should be gridSize + testCase.medium.materialIndexGrid = uint8(1); + no_dims = size(size(testCase.medium.soundSpeed),2); + testCase.verifyEqual(size(testCase.medium.soundSpeed),testCase.medium.gridSize(1:no_dims)) + + end + + function testAbsorptionPowerSetting(testCase) + + % No absorption sets map to NaNs + idx = testCase.materials.addMaterial('testTissue',struct('soundSpeed',1500, 'density',1000)); + testCase.medium.materialIndexGrid = idx; + testCase.verifyEqual(testCase.medium.absorptionPowerMap, single(NaN(testCase.kgrid.gridSize))) + + % Should have absorption of water + testCase.medium.materialIndexGrid = uint8(0); % water + v = testCase.medium.absorptionPower; + testCase.verifyEqual(v, testCase.materials.water.absorptionPower) + end end diff --git a/+kwave/+tests/+unit/TestSplitFieldPML.m b/+kwave/+tests/+unit/TestSplitFieldPML.m index b844e619..1f673c46 100644 --- a/+kwave/+tests/+unit/TestSplitFieldPML.m +++ b/+kwave/+tests/+unit/TestSplitFieldPML.m @@ -9,6 +9,22 @@ % vector field, and checks that the interior region doesn't change. All % dimensions and grid staggering options are tested. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef TestSplitFieldPML < kwave.tests.unit.AbstractTestGrid % Add PML to class properties so we can pass it to each testCase. @@ -18,7 +34,7 @@ % Setup PML for each test. methods(TestMethodSetup, ParameterCombination="sequential") - function createPML(testCase) + function createPML(testCase) testCase.pml = kwave.toolbox.SplitFieldPML(testCase.kgrid); testCase.pml.setupQuarticPML(1, 1500); end @@ -78,7 +94,7 @@ function testStaggeredPML(testCase) import matlab.unittest.constraints.IsEqualTo % Create test field. - vectorField = rand([testCase.kgridPadded.gridSize, testCase.kgridPadded.dimensions]); + vectorField = rand([testCase.kgridPadded.gridSize, testCase.kgridPadded.dimensions]); % Apply PML. vectorFieldAtten = testCase.pml.applyPML(vectorField, Staggered=true); diff --git a/+kwave/+tests/+unit/TestThermalMedium.m b/+kwave/+tests/+unit/TestThermalMedium.m index c4de8877..7f453bd6 100644 --- a/+kwave/+tests/+unit/TestThermalMedium.m +++ b/+kwave/+tests/+unit/TestThermalMedium.m @@ -4,6 +4,22 @@ % % Unit tests for the ThermalMedium class using the TestMedium class. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef TestThermalMedium < kwave.tests.unit.AbstractTestGridInput properties diff --git a/+kwave/+tests/+unit/TestThermalSolver.m b/+kwave/+tests/+unit/TestThermalSolver.m index 071fe380..ec6c8d87 100644 --- a/+kwave/+tests/+unit/TestThermalSolver.m +++ b/+kwave/+tests/+unit/TestThermalSolver.m @@ -8,9 +8,26 @@ % %% Description % Runs the following tests for the ThermalSolver: +% % * Verifies that simulations using input parameters with a different grid % throw errors. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef TestThermalSolver < matlab.unittest.TestCase methods(Test) diff --git a/+kwave/+tests/+unit/TestThermalSolverGrid.m b/+kwave/+tests/+unit/TestThermalSolverGrid.m index c0d18d82..87d0f448 100644 --- a/+kwave/+tests/+unit/TestThermalSolverGrid.m +++ b/+kwave/+tests/+unit/TestThermalSolverGrid.m @@ -7,8 +7,25 @@ % %% Description % Runs the following tests for the ThermalSolver: +% % * Verifies simulations in homogeneous media match exact solution +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef TestThermalSolverGrid < kwave.tests.unit.AbstractTestGrid % Parameterized tests. @@ -20,16 +37,17 @@ function initialValueProblemHomog(testCase) import matlab.unittest.constraints.IsEqualTo import kwave.toolbox.* - + % Medium. medium = ThermalMedium(testCase.kgrid); - medium.thermalConductivity = 0.52; - medium.specificHeat = 3540; + medium.thermalConductivity = 0.58; + medium.specificHeat = 4181; medium.density = 1000; - medium2 = Medium(testCase.kgrid); - medium2.materialIDGrid=1; - + materials = Materials(); + medium2 = Medium(testCase.kgrid,materials); + medium2.materialIndexGrid = uint8(0); + % Source. source = ThermalSource(testCase.kgrid); variance = (3 * testCase.kgrid.dx)^2; @@ -52,9 +70,9 @@ function initialValueProblemHomog(testCase) % Settings. settings = Settings; settings.plotSimulation = 'off'; - + % Solve using two steps. - solver = ThermalSolver(testCase.kgrid, medium, source, [], settings); + solver = ThermalSolver(testCase.kgrid, medium, source, [], settings); solver1 = ThermalSolver(testCase.kgrid, medium2, source, [], settings); Nt = 500; dt = 1; @@ -66,12 +84,12 @@ function initialValueProblemHomog(testCase) % Compute exact Green's function solution. D = medium.thermalConductivityPadded / (medium.densityPadded * medium.specificHeatPadded); - testCase.referenceSolution = kwave.legacy.bioheatExact(source.initialTemperaturePadded, 0, [D, 0, 0], testCase.kgrid.dx, (Nt) * dt); + testCase.referenceSolution = kwave.legacy.bioheatExact(source.initialTemperaturePadded, 0, [D, 0, 0], testCase.kgrid.dx, Nt * dt); % Compare with tolerance. testCase.verifyThat(testCase.actualSolution, IsEqualTo(testCase.referenceSolution, "Within", testCase.tol)); - - testCase.actualSolution = solver1.temperaturePadded; + + testCase.actualSolution = solver1.temperaturePadded; testCase.verifyThat(testCase.actualSolution, IsEqualTo(testCase.referenceSolution, "Within", testCase.tol)); % Take a step using auto-calculated Nt and dt. diff --git a/+kwave/+tests/+unit/TestThermalSource.m b/+kwave/+tests/+unit/TestThermalSource.m index d209634b..7ffc649d 100644 --- a/+kwave/+tests/+unit/TestThermalSource.m +++ b/+kwave/+tests/+unit/TestThermalSource.m @@ -7,6 +7,22 @@ % that the when given an initial condition it produces the correct size % matrix +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef TestThermalSource < kwave.tests.unit.AbstractTestGridInput properties @@ -36,8 +52,8 @@ function testSubrefs(testCase) source.initialTemperature = rand(source.gridSize); % Check slicing. testCase.verifyEqual(size(source.initialTemperature(1,:)), [1, 64]); - - + + end end diff --git a/+kwave/+tests/TestType.m b/+kwave/+tests/TestType.m index 59349901..f8526be4 100644 --- a/+kwave/+tests/TestType.m +++ b/+kwave/+tests/TestType.m @@ -13,12 +13,12 @@ % % * *kwave.tests.TestType.linting:* Tests for code quality. % * *kwave.tests.TestType.unit:* Tests for individual functions and class -% methods. +% methods. % % In future, other tests types will also be added: % % * *kwave.tests.TestType.integration:* Real world tests that use multiple -% functions or classes. +% functions or classes. % * *kwave.tests.TestType.regression:* Tests that outputs are stable over % time. % * *kwave.tests.TestType.benchmarks:* Tests for performance. @@ -28,6 +28,22 @@ %% See Also % * |runTests| +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef TestType enumeration linting; @@ -40,6 +56,6 @@ function folder = artifactsFolderName(obj) folder = string(obj) + '-tests-artifacts'; - end + end end end diff --git a/+kwave/+tests/runTests.m b/+kwave/+tests/runTests.m index 96ccb665..31a71687 100644 --- a/+kwave/+tests/runTests.m +++ b/+kwave/+tests/runTests.m @@ -1,4 +1,4 @@ -%% runTests +%% Run Tests % Run tests. % %% Syntax @@ -23,6 +23,22 @@ %% See Also % * |TestType| +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + function runTests(options) arguments diff --git a/+kwave/+toolbox/@AcousticMedium/AcousticMedium.m b/+kwave/+toolbox/@AcousticMedium/AcousticMedium.m index fdaa9c57..9f7afa79 100644 --- a/+kwave/+toolbox/@AcousticMedium/AcousticMedium.m +++ b/+kwave/+toolbox/@AcousticMedium/AcousticMedium.m @@ -27,23 +27,42 @@ % medium.absorptionPower = rand(); % %% Properties +% % * |soundSpeed| - (single) Compressional sound speed [m/s]. % * |soundSpeedReference| - (single) Reference compressional sound speed % used in the k-space correction [m/s]. Automatically defined in % kWaveAcoustic if not defined by the user. % * |density| - (single) Mass density [kg/m^2]. % * |absorptionCoeff| - (single) Power law attenuation coefficient -% [dB/(MHz^y cm)]. +% [dB/(MHz^y cm)]. % * |absorptionPower| - (single scalar) Power law attenuation power. % * |BonA| - (single) Parameter of nonlinearity. % % In order to model absorption both absorptionCoeff, and absorptionPower -% must be defiened, and solver.absorptionType must be set for the acoustic +% must be defined, and solver.absorptionType must be set for the acoustic % solver +% %% See Also +% % * |GridInput| % * |AcousticSolver| +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef AcousticMedium < kwave.toolbox.GridInput properties diff --git a/+kwave/+toolbox/@AcousticSensor/AcousticSensor.m b/+kwave/+toolbox/@AcousticSensor/AcousticSensor.m new file mode 100644 index 00000000..7e16a765 --- /dev/null +++ b/+kwave/+toolbox/@AcousticSensor/AcousticSensor.m @@ -0,0 +1,179 @@ +%% AcousticSensor +% *Package:* kwave.toolbox +% *Superclasses:* kwave.toolbox.Sensor +% +% Class used to define acoustic sensors +% +%% Syntax +% source = AcousticSensor(kgrid); +% +%% Description +% This class is used to define acoustic sensors. The constructor +% takes an object of the |kwave.toolbox.Sensor| class, which defines the +% grid size. +% +%% Examples +% Define the grid and sensor objects, and a binary sensor mask containing a +% single sensor position. +% +% kgrid = kwave.toolbox.Grid([128, 128], 1e-3); +% sensor = kwave.toolbox.AcousticSensor(kgrid); +% sensor.pressureSensor = 'on'; +% sensor.mask = zeros(sensor.gridSize); +% sensor.mask(1, :) = 1; +% +% kgrid = kwave.toolbox.Grid([128, 128], 1e-3); +% sensor = kwave.toolbox.AcousticSensor(kgrid); +% sensor.pressureSensor = 'off'; +% sensor.velocitySensor = 'on'; +% sensor.mask = zeros(sensor.gridSize); +% sensor.mask(1, :) = 1; +% sensor.timeStepSpacing = 2; +% +%% Properties (pre-simulation) +% * |pressureSensor| - Sets the acoustic pressure to be recorded ('on' / +% 'off'). Default 'on'. +% * |velocitySensor| - Sets the acoustic particle velocity to be recorded +% ('on' / 'off' / 'ongrid'). By default the particle +% velocity is returned on the staggered grid and at +% staggered times. The 'ongrid' option returns it on +% the grid (same grid as the pressure) but still at +% staggered times. Default 'off'. +% * |densitySensor| - Sets the acoustic density to be recorded ('on' / +% 'off'). Default 'off'. +% * |mask| - Binary mask the size of the grid with 1s indicated +% sensor positions. +% * |timeStepSpacing| - Number of timesteps separating recording events. If +% this is 1 then the sensors record at every +% timestep. If 2 then at every other timestep, etc. +% +%% Properties (post-simulation) +% +% * |pressure| - Matrix (number of sensor points x number of time points) +% containing the values of the acoustic pressure at the +% sensor positions at the times given in |times|. The sensor +% positions are indexed using Matlab's column-major indexing. +% * |velocity| - Matrix of values of the acoustic particle velocity at the +% sensor positions at the times given in |times|. +% * |density| - Matrix of values of the acoustic density at the sensor +% positions at the times given in |times|. +% * |times| - Vector of recording times. +% +%% See Also +% * |Sensor|, |GridInput| + +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + +classdef AcousticSensor < kwave.toolbox.Sensor + + properties + pressureSensor char {mustBeMember( pressureSensor, {'on','off'})} = 'on' + velocitySensor char {mustBeMember( velocitySensor, {'on','off','ongrid'})} = 'off' + densitySensor char {mustBeMember( densitySensor, {'on','off'})} = 'off' + timeStepSpacing (1,1) {mustBeInteger} = 1 + + pressure = []; + velocity = []; + density = []; + times = []; + end + + methods(Access=public) + + function obj=initialiseSensorData(obj,Nt) + id=0; + if Nt==0 + if strcmp(obj.pressureSensor,'on') && isempty(obj.pressure) + id=1; + obj.pressure = single(zeros(obj.totalSensorPoints,1)); + end + if strcmp(obj.densitySensor,'on') && isempty(obj.density) + id=1; + obj.density = single(zeros(obj.totalSensorPoints,1)); + end + if (strcmp(obj.velocitySensor,'on') || strcmp(obj.velocitySensor,'ongrid')) && isempty(obj.velocity) + id=1; + obj.velocity = single(zeros(obj.totalSensorPoints,obj.kgrid.dimensions,1)); + end + if id==1 + obj.times=0; + end + else + if strcmp(obj.pressureSensor,'on') + if isempty(obj.pressure) + id=1; + obj.pressure=[zeros(obj.totalSensorPoints,floor(Nt/obj.timeStepSpacing)+1)]; + else + id=2; + obj.pressure=[obj.pressure,zeros(obj.totalSensorPoints,floor(Nt/obj.timeStepSpacing))]; + end + end + if strcmp(obj.densitySensor,'on') + if isempty(obj.density) + id=1; + obj.density=[zeros(obj.totalSensorPoints,floor(Nt/obj.timeStepSpacing)+1)]; + else + id=2; + obj.density=[obj.density,zeros(obj.totalSensorPoints,floor(Nt/obj.timeStepSpacing))]; + end + end + if strcmp(obj.velocitySensor,'on') || strcmp(obj.velocitySensor,'ongrid') + if isempty(obj.velocity) + id=1; + obj.velocity=[zeros(obj.totalSensorPoints,obj.kgrid.dimensions,floor(Nt/obj.timeStepSpacing)+1)]; + else + id=2; + obj.velocity=cat(3,obj.velocity,zeros(obj.totalSensorPoints,obj.kgrid.dimensions,floor(Nt/obj.timeStepSpacing))); + end + end + if id==1 + obj.times=zeros(1,floor(Nt/obj.timeStepSpacing)+1); + elseif id==2 + obj.times=[obj.times,zeros(1,floor(Nt/obj.timeStepSpacing))]; + end + + end + end + + % Assign the acoustic variables to the sensors + function obj = recordSensorData(obj,solver,n) + + import kwave.toolbox.FourierCollocation + + if strcmp(obj.pressureSensor,'on') + obj.pressure(:,n) = obj.ProcessSensorData(solver.pressure,1,'none'); + end + if strcmp(obj.densitySensor,'on') + obj.density(:,n) = obj.ProcessSensorData(solver.densitySplit,obj.kgrid.dimensions,'sum'); + end + if strcmp(obj.velocitySensor,'on') + obj.velocity(:,:,n) = obj.ProcessSensorData(solver.velocity,obj.kgrid.dimensions,'none'); + elseif strcmp(obj.velocitySensor,'ongrid') + GridVelocityPadded = zeros(size(solver.velocityPadded)); + for dim=1:solver.kgrid.dimensions + GridVelocityPadded(:,:,:,dim) = stagger(solver,solver.velocityPadded(:,:,:,dim),Staggering='backward'); + end + obj.velocity(:,:,n) = obj.ProcessSensorData(solver.kgrid.returnWithoutGridPadding(GridVelocityPadded),obj.kgrid.dimensions,'none'); + end + obj.times(n) = solver.timePoint; + end + + end + + + +end diff --git a/+kwave/+toolbox/@AcousticSolver/AcousticSolver.m b/+kwave/+toolbox/@AcousticSolver/AcousticSolver.m index d8ca3a55..69733930 100644 --- a/+kwave/+toolbox/@AcousticSolver/AcousticSolver.m +++ b/+kwave/+toolbox/@AcousticSolver/AcousticSolver.m @@ -44,29 +44,29 @@ % % clearvars; % import kwave.toolbox.* -% +% % % Grid. % kgrid = Grid(128, 1e-3, 20); -% +% % % Medium. % medium = AcousticMedium(kgrid); % medium.soundSpeed = 1500; % medium.density = 1000; -% +% % % Source. % source = AcousticSource(kgrid); % source.initialPressure = exp( -kgrid.xVec.^2 ./ (10 * kgrid.dx).^2 ); -% +% % % Settings. % settings = Settings; % settings.plotFrequency = 1; -% +% % % Solve. % solver = AcousticSolver(kgrid, medium, source, [], settings); % CFL = 0.5; % dt = CFL * kgrid.dx / medium.soundSpeed; % solver.run(Nt=80, dt=dt); -% +% % % Plot. % figure; % plot(1e3 * kgrid.xVec, source.initialPressure); @@ -85,7 +85,8 @@ % medium properties. % * |source| - (kwave.toolbox.AcousticSource) Object which defines the % source properties. -% * |sensor| - ...Not yet implemented... +% * |sensor| - (kwave.toolbox.AcousticSensor) Object which defines the +% sensor properties. % * |settings| - (kwave.toolbox.Settings) Object which defines the % simulation settings. % @@ -100,10 +101,14 @@ % exclude each of the absorption terms within the equation of state. % %% Methods -% * |setkSpaceCorrection| -% * |setAbsorptionCoefficients| +% * |autoComputeTimeStep| +% * |executeTimeStep| % * |kappaSplitCorrection| -% +% * |setAbsorptionCoefficients| +% * |setInitialConditions| +% * |setkSpaceCorrection| + + % Copyright (C) 2024- The k-Wave Authors. % % This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free @@ -111,12 +116,12 @@ % GNU Lesser General Public License as published by the Free Software % Foundation, either version 3 of the License, or (at your option) any % later version. -% +% % k-Wave-II is distributed in the hope that it will be useful, but WITHOUT % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or % FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public % License for more details. -% +% % You should have received a copy of the GNU Lesser General Public License % along with k-Wave-II. If not, see . @@ -136,7 +141,7 @@ velocityPadded single pml kwave.toolbox.SplitFieldPML end - + properties(SetAccess=public,Hidden=false) absorptionType char {mustBeMember( absorptionType, {'off','on', 'noAbsorption', 'noDispersion'})} = 'off' end @@ -145,29 +150,37 @@ absorbTauPadded single absorbEtaPadded single kappaSplit single + + timePoint = 0; end % Constructor. methods function obj = AcousticSolver(kgrid, medium, source, sensor, settings) + arguments - kgrid(1,1) kwave.toolbox.Grid - medium(1,1) kwave.toolbox.GridInput - source(1,1) kwave.toolbox.AcousticSource + kgrid(1,1) kwave.toolbox.Grid + medium(1,1) kwave.toolbox.GridInput + source(1,1) kwave.toolbox.AcousticSource sensor settings(1,1) kwave.toolbox.Settings = kwave.toolbox.Settings end - - if ~(isa(medium, 'kwave.toolbox.Medium') || isa(medium, 'kwave.toolbox.AcousticMedium')) - error('AcousticSolver:InvalidMediumType',... - 'medium must be an object of Medium OR AcousticMedium.'); - end + + % Check medium and sensor inputs are of the right class + if ~(isa(medium, 'kwave.toolbox.Medium') || isa(medium, 'kwave.toolbox.AcousticMedium')) + error('AcousticSolver:InvalidMediumType',... + 'medium must be an object of Medium or AcousticMedium class.'); + end + if ~(isempty(sensor) || isa(sensor, 'kwave.toolbox.AcousticSensor')) + error('AcousticSolver:InvalidSensorType',... + 'sensor must be an AcousticSensor object or be left empty [].'); + end % Pass input arguments to superclass constructor. This calls - % setInitialConditions. - obj@kwave.toolbox.TimeDomainSolver(kgrid, medium, source, sensor, settings); - % Initialise PML object. - obj.pml = kwave.toolbox.SplitFieldPML(obj.kgrid); + % setInitialConditions. + obj@kwave.toolbox.TimeDomainSolver(kgrid, medium, source, sensor, settings); + % Initialise PML object. + obj.pml = kwave.toolbox.SplitFieldPML(obj.kgrid); end end @@ -183,7 +196,7 @@ velocity = obj.kgrid.returnWithoutGridPadding(obj.velocityPadded); end end - + % Override inherited methods and add specific methods. methods(Access=protected) setInitialConditions(obj) diff --git a/+kwave/+toolbox/@AcousticSolver/autoComputeTimeStep.m b/+kwave/+toolbox/@AcousticSolver/autoComputeTimeStep.m index ab2dec94..816e6156 100644 --- a/+kwave/+toolbox/@AcousticSolver/autoComputeTimeStep.m +++ b/+kwave/+toolbox/@AcousticSolver/autoComputeTimeStep.m @@ -1,4 +1,4 @@ -%% autoComputeTimeStep +%% autoCompute Time Step % *Class:* kwave.toolbox.AcousticSolver % *Package:* kwave.toolbox % @@ -26,12 +26,12 @@ % GNU Lesser General Public License as published by the Free Software % Foundation, either version 3 of the License, or (at your option) any % later version. -% +% % k-Wave-II is distributed in the hope that it will be useful, but WITHOUT % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or % FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public % License for more details. -% +% % You should have received a copy of the GNU Lesser General Public License % along with k-Wave-II. If not, see . @@ -39,7 +39,7 @@ arguments obj - CFL {mustBeScalarOrEmpty, mustBePositive, mustBeFinite} + CFL {mustBeScalarOrEmpty, mustBePositive, mustBeFinite} endTime {mustBeScalarOrEmpty, mustBePositive, mustBeFinite} end @@ -63,7 +63,7 @@ dt = CFL .* dx ./ soundSpeedMax; % Compute Nt based on EndTime. -Nt = ceil(endTime / dt); +Nt = uint64(ceil(endTime / dt)); % Recompute dt to ensure that EndTime is exactly reached. -dt = endTime / Nt; +dt = endTime / single(Nt); diff --git a/+kwave/+toolbox/@AcousticSolver/executeTimeStep.m b/+kwave/+toolbox/@AcousticSolver/executeTimeStep.m index 44cc8d7e..f90ecad8 100644 --- a/+kwave/+toolbox/@AcousticSolver/executeTimeStep.m +++ b/+kwave/+toolbox/@AcousticSolver/executeTimeStep.m @@ -1,4 +1,4 @@ -%% executeTimeStep +%% execute Time Step % *Class:* kwave.toolbox.AcousticSolver % *Package:* kwave.toolbox % @@ -31,11 +31,12 @@ % You should have received a copy of the GNU Lesser General Public License % along with k-Wave-II. If not, see . + function executeTimeStep(obj, Nt, dt) arguments obj - Nt(1,1) {mustBeInteger, mustBeNonnegative, mustBeFinite} + Nt(1,1) uint32 dt(1,1) {mustBeNumeric, mustBePositive, mustBeFinite} end @@ -62,6 +63,34 @@ function executeTimeStep(obj, Nt, dt) gradient = @(x) obj.gradient(x, Staggering='forward'); divergence = @(x) obj.divergenceSplit(x, Staggering='backward'); + +% Build effective (NaN-safe) absorption inputs. + function [hasAbsorption, tauEff, etaEff, yEff] = buildAbsorptionOperators() + + % Short-circuit if globally off + if strcmp(obj.absorptionType, 'off') + hasAbsorption = false; tauEff = []; etaEff = []; yEff = []; + return; + end + + % Current design uses a scalar exponent + yScalar = obj.medium.absorptionPower; % scalar, may be NaN + + % Start from the padded operators (as built by setAbsorptionCoefficients) + tauEff = obj.absorbTauPadded; % may contain NaN where material missing + etaEff = obj.absorbEtaPadded; % may contain NaN where material missing + + % Zero-out invalid voxels in the operators (lossless locally) + tauEff(~isfinite(tauEff)) = 0; + etaEff(~isfinite(etaEff)) = 0; + + % Enable absorption only if something remains + hasAbsorption = any(tauEff(:) ~= 0 | etaEff(:) ~= 0); + yEff = yScalar; % keep scalar exponent + end + + +% Stagger the density, if required if length(obj.medium.densityPadded)~= 1 densityPaddedStg=obj.stagger(obj.medium.densityPadded,Stagger='forward', Type='linInterpolate'); else @@ -72,15 +101,19 @@ function executeTimeStep(obj, Nt, dt) fig = figure; end +% no time index adjustment by default +adj = uint64(0); + if (obj.timeStepsTaken == 0) - % Adds a time step if initial conditions need applying. - Nt=Nt+1; + % Adds a time step if initial conditions need to be applied. + Nt = Nt+1; + adj = uint64(1); end if Nt~=0 %If no time steps are taken for a system that has been run then the %original solution is passed back out. - for tIndex = 1:Nt + for tIndex = uint64(1:Nt) if (tIndex == 1) if (obj.timeStepsTaken == 0) @@ -88,24 +121,24 @@ function executeTimeStep(obj, Nt, dt) if isempty(obj.source.initialVelocity) % Sets initial conditions with velocity(t=0)=0 through % assuming V(-t)=v(t). - obj.pressurePadded = obj.pressurePadded + obj.source.initialPressurePadded; + obj.pressurePadded = obj.pressurePadded + obj.source.initialPressurePadded; obj.densitySplitPadded = obj.densitySplitPadded + obj.source.initialPressurePadded ./ (obj.dimensions * obj.medium.soundSpeedPadded.^2); - obj.velocityPadded = (dt ./ densityPaddedStg) .* gradient(obj.pressurePadded) / 2; + obj.velocityPadded = (dt ./ densityPaddedStg) .* gradient(obj.pressurePadded) / 2; else - % If Velocity Initial condition is given the initial + % If velocity initial condition is given the initial % conditions account for staggering and the offset time % stepping - obj.pressurePadded = obj.pressurePadded + obj.source.initialPressurePadded; + obj.pressurePadded = obj.pressurePadded + obj.source.initialPressurePadded; obj.densitySplitPadded = obj.densitySplitPadded + obj.source.initialPressurePadded ./ (obj.dimensions * obj.medium.soundSpeedPadded.^2); - - initialVelocityDimensional=zeros([obj.kgridPadded.gridSize,obj.kgrid.dimensions])+obj.source.initialVelocityPadded; + + initialVelocityDimensional = zeros([obj.kgridPadded.gridSize,obj.kgrid.dimensions]) + obj.source.initialVelocityPadded; for dim=1:obj.kgrid.dimensions - initialVelcoityStaggered=obj.stagger(initialVelocityDimensional(:,:,:,dim), Type='fourier'); + initialVelcoityStaggered = obj.stagger(initialVelocityDimensional(:,:,:,dim), Type='fourier'); obj.velocityPadded(:,:,:,dim) = obj.velocityPadded(:,:,:,dim) + initialVelcoityStaggered(:,:,:,dim); end clear('initialVelcoityStaggered','initialVelocityDimensional') - obj.prevTimeStep=0; + obj.prevTimeStep = 0; dt = (currentTimeStep)/2; obj.pml.setupQuarticPML(dt, obj.medium.soundSpeedReference); obj.setkSpaceCorrection(currentTimeStep); @@ -126,11 +159,20 @@ function executeTimeStep(obj, Nt, dt) % Pressure density relation. obj.pressurePadded = obj.medium.soundSpeedPadded.^2 .* ( sum(obj.densitySplitPadded, 4)); - % If absorptionPower declaired then add absorption terms - if ~strcmp(obj.absorptionType,'off') - obj.pressurePadded = obj.pressurePadded + obj.medium.soundSpeedPadded.^2 .* ( ... - obj.absorbTauPadded .* fracLaplacian(obj, obj.medium.densityPadded .* sum(divergence(obj.velocityPadded),4), obj.medium.absorptionPower/2 -1 ) + ... - obj.absorbEtaPadded .* fracLaplacian(obj, sum(obj.densitySplitPadded,4), obj.medium.absorptionPower/2 -0.5 ) ) ; + % Add absorption terms if required + [doAbs, tauEff, etaEff, yEff] = buildAbsorptionOperators(); + if doAbs + obj.pressurePadded = obj.pressurePadded + ... + obj.medium.soundSpeedPadded.^2 .* ( ... + tauEff .* fracLaplacian( ... + obj, ... + obj.medium.densityPadded .* sum(divergence(obj.velocityPadded),4), ... + (yEff/2 - 1) ) + ... + etaEff .* fracLaplacian( ... + obj, ... + sum(obj.densitySplitPadded,4), ... + (yEff/2 - 0.5) ) ... + ); end end elseif (tIndex==2) && (obj.timeStepsTaken == 0) && ~isempty(obj.source.initialVelocity) @@ -150,11 +192,20 @@ function executeTimeStep(obj, Nt, dt) % Pressure density relation. obj.pressurePadded = obj.medium.soundSpeedPadded.^2 .* ( sum(obj.densitySplitPadded, 4)); - % If absorptionPower declaired then add absorption terms - if ~strcmp(obj.absorptionType,'off') - obj.pressurePadded = obj.pressurePadded + obj.medium.soundSpeedPadded.^2 .* ( ... - obj.absorbTauPadded .* fracLaplacian(obj, obj.medium.densityPadded .* sum(divergence(obj.velocityPadded),4), obj.medium.absorptionPower/2 -1 ) + ... - obj.absorbEtaPadded .* fracLaplacian(obj, sum(obj.densitySplitPadded,4), obj.medium.absorptionPower/2 -0.5 ) ) ; + % If required add absorption terms + [doAbs, tauEff, etaEff, yEff] = buildAbsorptionOperators(); + if doAbs + obj.pressurePadded = obj.pressurePadded + ... + obj.medium.soundSpeedPadded.^2 .* ( ... + tauEff .* fracLaplacian( ... + obj, ... + obj.medium.densityPadded .* sum(divergence(obj.velocityPadded),4), ... + (yEff/2 - 1) ) + ... + etaEff .* fracLaplacian( ... + obj, ... + sum(obj.densitySplitPadded,4), ... + (yEff/2 - 0.5) ) ... + ); end else @@ -169,16 +220,36 @@ function executeTimeStep(obj, Nt, dt) % Pressure density relation. obj.pressurePadded = obj.medium.soundSpeedPadded.^2 .* ( sum(obj.densitySplitPadded, 4)); - % If absorptionPower declaired then add absorption terms - if ~strcmp(obj.absorptionType,'off') - obj.pressurePadded = obj.pressurePadded + obj.medium.soundSpeedPadded.^2 .* ( ... - obj.absorbTauPadded .* fracLaplacian(obj, obj.medium.densityPadded .* sum(divergence(obj.velocityPadded),4), obj.medium.absorptionPower/2 -1 ) + ... - obj.absorbEtaPadded .* fracLaplacian(obj, sum(obj.densitySplitPadded,4), obj.medium.absorptionPower/2 -0.5 ) ) ; + % If required add absorption terms + [doAbs, tauEff, etaEff, yEff] = buildAbsorptionOperators(); + if doAbs + obj.pressurePadded = obj.pressurePadded + ... + obj.medium.soundSpeedPadded.^2 .* ( ... + tauEff .* fracLaplacian( ... + obj, ... + obj.medium.densityPadded .* sum(divergence(obj.velocityPadded),4), ... + (yEff/2 - 1) ) + ... + etaEff .* fracLaplacian( ... + obj, ... + sum(obj.densitySplitPadded,4), ... + (yEff/2 - 0.5) ) ... + ); + end + end + + % + if ~isempty(obj.sensor) && rem(tIndex-adj, obj.sensor.timeSteps) == 0 + if ~isempty(obj.timeArray) + obj.timePoint = obj.timeArray(end) + dt*single(tIndex-adj); + else + obj.timePoint = dt * single(tIndex-adj); end + obj.sensor.sensorIndex=obj.sensor.sensorIndex+1; + obj.sensor.recordSensorData(obj,obj.sensor.sensorIndex); end % Plot. - if obj.settings.plotSimulation && (rem(tIndex, obj.settings.plotFrequency) == 0 || tIndex == 1 || tIndex == Nt) + if obj.settings.plotSimulation && (rem(tIndex-adj, obj.settings.plotFrequency) == 0 || tIndex == 1 || tIndex == Nt) figure(fig); obj.plotField(obj.pressure); end diff --git a/+kwave/+toolbox/@AcousticSolver/kappaSplitCorrection.m b/+kwave/+toolbox/@AcousticSolver/kappaSplitCorrection.m index 81fece4d..39aab514 100644 --- a/+kwave/+toolbox/@AcousticSolver/kappaSplitCorrection.m +++ b/+kwave/+toolbox/@AcousticSolver/kappaSplitCorrection.m @@ -1,46 +1,62 @@ -%% kappaSplitCorrection -% *Class:* kwave.toolbox.AcousticSolver -% *Package:* kwave.toolbox -% -% Calculate the temporal correction of a function in the time step -% variation -% -%% Syntax -% fC = kappaSplitCorrection(obj, f) -% -%% Description -% Calculates the correction to the usual kspace corrected tempral -% derivative for using an varied time stepping by considering a first -% order interpolation. -% -% The vector components of the correction are stacked in the 4th dimension -% as with the gradient. For example, if calling gradient on a matrix of dimensions -% (10, 10), the output will be of size (10, 10, 1, 2). This is to allow -% codes to implement multi-dimensional support by always looping over the -% fourth dimension. -% -% The components are calculated using 1D FFTs, and kappa2 is applied in the -% Fourier domain. before inverting the Fourier Transform. -% -% -%% Input Arguments -% * |f| - (numeric) Scalar field that is to be corrected using. -% -%% Output Arguments -% * |fCorrected| - (numeric) Correction from f. - - -function fCorrected = kappaSplitCorrection(obj, f) - -arguments - obj - f(:,:,:,:) -end - -fCorrected = zeros([obj.kgridPadded.gridSize, obj.dimensions]); - -for dim=1:obj.dimensions - fCorrected(:, :, :,dim) = ifftn(obj.kappaSplit .* fftn(f(:,:,:,dim)), 'symmetric'); -end - -end \ No newline at end of file +%% kappa Split Correction +% *Class:* kwave.toolbox.AcousticSolver +% *Package:* kwave.toolbox +% +% Calculate the temporal correction of a function in the time step +% variation +% +%% Syntax +% fC = kappaSplitCorrection(obj, f) +% +%% Description +% Calculates the correction to the usual kspace corrected temporal +% derivative for using an varied time stepping by considering a first +% order interpolation. +% +% The vector components of the correction are stacked in the 4th dimension +% as with the gradient. For example, if calling gradient on a matrix of dimensions +% (10, 10), the output will be of size (10, 10, 1, 2). This is to allow +% codes to implement multi-dimensional support by always looping over the +% fourth dimension. +% +% The components are calculated using 1D FFTs, and kappa2 is applied in the +% Fourier domain. before inverting the Fourier Transform. +% +% +%% Input Arguments +% * |f| - (numeric) Scalar field that is to be corrected using. +% +%% Output Arguments +% * |fCorrected| - (numeric) Correction from f. + +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + + +function fCorrected = kappaSplitCorrection(obj, f) + +arguments + obj + f(:,:,:,:) +end + +fCorrected = zeros([obj.kgridPadded.gridSize, obj.dimensions]); + +for dim=1:obj.dimensions + fCorrected(:, :, :,dim) = ifftn(obj.kappaSplit .* fftn(f(:,:,:,dim)), 'symmetric'); +end + +end diff --git a/+kwave/+toolbox/@AcousticSolver/setAbsorptionCoefficients.m b/+kwave/+toolbox/@AcousticSolver/setAbsorptionCoefficients.m index bcfa5ec7..05ab7925 100644 --- a/+kwave/+toolbox/@AcousticSolver/setAbsorptionCoefficients.m +++ b/+kwave/+toolbox/@AcousticSolver/setAbsorptionCoefficients.m @@ -1,25 +1,23 @@ -%% SetAbsorptionCoefficients -% *Class:* kwave.toolbox.AcosticSolver +%% Set Absorption Coefficients +% *Class:* kwave.toolbox.AcousticSolver % *Package:* kwave.toolbox % -% Compute and assign the coefficents tau and eta in the fractional laplacian loss. +% Compute and assign the coefficients tau and eta in the fractional laplacian loss. % %% Syntax -% setabsorptioncoefficients(obj) +% setAbsorptionCoefficients(obj) % %% Description -% Compute and assign the coefficents tau and eta in the fractional, -% assigns values as given by the formula in: -% Treeby, Bradley E., and Ben T. Cox. -% "Modeling power law absorption and dispersion for acoustic propagation using the fractional Laplacian." -% The Journal of the Acoustical Society of America 127.5 (2010): 2741-2748. +% Compute and assign the coefficients tau and eta in the fractional, +% assigns values as given by the formula in: % -% If no dispersion, problem reduces with eta=0 -% If no absorption, problem reduces with tau=0 -% 'noDispersion' or 'noAbsorption' or 'on' must be declaired by setting -% AcousticSolver.absorptionType +% Treeby, Bradley E., and Ben T. Cox. +% "Modeling power law absorption and dispersion for acoustic propagation using the fractional Laplacian." +% The Journal of the Acoustical Society of America 127.5 (2010): 2741-2748. % -% If absorptionPower=0, 2, problem reduces. +% If no dispersion, problem reduces with eta=0. If no absorption, problem +% reduces with tau=0. 'noDispersion' or 'noAbsorption' or 'on' must be declared by setting +% |AcousticSolver.absorptionType|. If absorptionPower=0, 2, problem reduces. % % |obj.prevTimeStep|, the k-space correction is adjusted to account for the % change in time step. @@ -27,23 +25,24 @@ %% Input Arguments % None % -% Copyright (C) 2024- The k-Wave-II Authors. + +% Copyright (C) 2024- The k-Wave Authors. % % This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free % software: you can redistribute it and/or modify it under the terms of the % GNU Lesser General Public License as published by the Free Software % Foundation, either version 3 of the License, or (at your option) any % later version. -% +% % k-Wave-II is distributed in the hope that it will be useful, but WITHOUT % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or % FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public % License for more details. -% +% % You should have received a copy of the GNU Lesser General Public License % along with k-Wave-II. If not, see . + % -%% ======================================================================== function setAbsorptionCoefficients(obj) @@ -70,5 +69,3 @@ function setAbsorptionCoefficients(obj) end end - - diff --git a/+kwave/+toolbox/@AcousticSolver/setInitialConditions.m b/+kwave/+toolbox/@AcousticSolver/setInitialConditions.m index cc39d666..8ce361e6 100644 --- a/+kwave/+toolbox/@AcousticSolver/setInitialConditions.m +++ b/+kwave/+toolbox/@AcousticSolver/setInitialConditions.m @@ -1,4 +1,4 @@ -%% setInitialConditions +%% set Initial Conditions % *Class:* kwave.toolbox.AcousticSolver % *Package:* kwave.toolbox % @@ -13,7 +13,7 @@ % conditions for a photoacoustic source defined by |source.initialPressure| % are setup in |kwave.toolbox.AcousticSolver.executeTimeStep| as this % relies on the time step size. -% +% % The reference sound speed |obj.medium.soundSpeedReference| is also % assigned if not provided. The value is set to maximum value in % |obj.medium.soundSpeed| which means the scheme will be unconditionally @@ -26,12 +26,12 @@ % GNU Lesser General Public License as published by the Free Software % Foundation, either version 3 of the License, or (at your option) any % later version. -% +% % k-Wave-II is distributed in the hope that it will be useful, but WITHOUT % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or % FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public % License for more details. -% +% % You should have received a copy of the GNU Lesser General Public License % along with k-Wave-II. If not, see . diff --git a/+kwave/+toolbox/@AcousticSolver/setkSpaceCorrection.m b/+kwave/+toolbox/@AcousticSolver/setkSpaceCorrection.m index 06281ac1..d8c2e5ce 100644 --- a/+kwave/+toolbox/@AcousticSolver/setkSpaceCorrection.m +++ b/+kwave/+toolbox/@AcousticSolver/setkSpaceCorrection.m @@ -1,57 +1,57 @@ -%% setkSpaceCorrection -% *Class:* kwave.toolbox.AcousticSolver -% *Package:* kwave.toolbox -% -% Compute and assign the k-space dispersion correction. -% -%% Syntax -% setkSpaceCorrection(obj, dt) -% -%% Description -% Computes the k-space dispersion correction for the wave equation and -% assigns this to obj.kappa. The calculation is based on -% |obj.medium.soundSpeedReference|. If |dt| is not equal to -% |obj.prevTimeStep|, the k-space correction is adjusted to account for the -% change in time step. -% -%% Input Arguments -% * |dt| - (numeric) Size of each time step. - -% Copyright (C) 2024- The k-Wave Authors. -% -% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free -% software: you can redistribute it and/or modify it under the terms of the -% GNU Lesser General Public License as published by the Free Software -% Foundation, either version 3 of the License, or (at your option) any -% later version. -% -% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT -% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public -% License for more details. -% -% You should have received a copy of the GNU Lesser General Public License -% along with k-Wave-II. If not, see . - -function setkSpaceCorrection(obj, dt) - -arguments - obj - dt(1,1) {mustBeNumeric, mustBePositive, mustBeFinite} -end - -ck = obj.medium.soundSpeedReference * obj.kgridPadded.k; - -if (dt ~= obj.prevTimeStep) - dt2 = dt; - dt1 = obj.prevTimeStep; - - obj.kappaSplit = ifftshift ( 2 * (cos( ck * dt2/2) ./ cos( ck * dt1/2) -1) ./ ((dt1 + dt2)) ); - obj.kappa = ifftshift ( 2 * (sin( ck * dt2/2) + sin( ck * dt1/2).* (cos( ck * dt2/2) ./ cos( ck * dt1/2) ) )./ (ck * (dt1 + dt2))); - obj.kappa(isnan(obj.kappa)) = 1; - obj.kappaSplit(isnan(obj.kappaSplit)) = 1; - -else - obj.kappa = ifftshift(kwave.toolbox.FourierCollocation.sinc(ck * dt/2)); - obj.kappaSplit=0; -end +%% set kSpace Correction +% *Class:* kwave.toolbox.AcousticSolver +% *Package:* kwave.toolbox +% +% Compute and assign the k-space dispersion correction. +% +%% Syntax +% setkSpaceCorrection(obj, dt) +% +%% Description +% Computes the k-space dispersion correction for the wave equation and +% assigns this to obj.kappa. The calculation is based on +% |obj.medium.soundSpeedReference|. If |dt| is not equal to +% |obj.prevTimeStep|, the k-space correction is adjusted to account for the +% change in time step. +% +%% Input Arguments +% * |dt| - (numeric) Size of each time step. + +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + +function setkSpaceCorrection(obj, dt) + +arguments + obj + dt(1,1) {mustBeNumeric, mustBePositive, mustBeFinite} +end + +ck = obj.medium.soundSpeedReference * obj.kgridPadded.k; + +if (dt ~= obj.prevTimeStep) + dt2 = dt; + dt1 = obj.prevTimeStep; + + obj.kappaSplit = ifftshift ( 2 * (cos( ck * dt2/2) ./ cos( ck * dt1/2) -1) ./ ((dt1 + dt2)) ); + obj.kappa = ifftshift ( 2 * (sin( ck * dt2/2) + sin( ck * dt1/2).* (cos( ck * dt2/2) ./ cos( ck * dt1/2) ) )./ (ck * (dt1 + dt2))); + obj.kappa(isnan(obj.kappa)) = 1; + obj.kappaSplit(isnan(obj.kappaSplit)) = 1; + +else + obj.kappa = ifftshift(kwave.toolbox.FourierCollocation.sinc(ck * dt/2)); + obj.kappaSplit=0; +end diff --git a/+kwave/+toolbox/@AcousticSource/AcousticSource.m b/+kwave/+toolbox/@AcousticSource/AcousticSource.m index d15b31d5..1d8a62fe 100644 --- a/+kwave/+toolbox/@AcousticSource/AcousticSource.m +++ b/+kwave/+toolbox/@AcousticSource/AcousticSource.m @@ -2,15 +2,15 @@ % *Package:* kwave.toolbox % *Superclasses:* kwave.toolbox.GridInput % -% Class used to define the acoustic sources for a simulation. +% Class used to define acoustic sources. % %% Syntax % source = AcousticSource(kgrid); % %% Description -% This class is used to define the acoustic source terms. The constructor -% takes an object of the |kwave.toolbox.Grid| class which defines the grid -% size. Source matrices must match the grid size defined by kgrid. +% This class is used to define the acoustic source terms (which includes +% initial conditions). The constructor takes an object of the +% |kwave.toolbox.Grid| class, which defines the grid size. % %% Examples % Define the grid and source objects, and assign the initial pressure. @@ -19,22 +19,43 @@ % source = kwave.toolbox.AcousticSource(kgrid); % source.initialPressure = rand(source.gridSize); % -%% Properties -% * |initialPressure| - (numeric) Initial pressure distribution [Pa]. This -% assumes the initial particle velocity is zero, which is a equivalent to -% a photoacoustic source under the conditions of stress confinement. +%% Required Properties +% * |initialPressure| - Initial acoustic pressure distribution [Pa]. +% +%% Optional Properties +% * |initialVelocity| - Initial acoustic particle velocity distribution [m/s]. +% If this is not defined, it is assumed to be zero. % %% See Also % * |GridInput| +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef AcousticSource < kwave.toolbox.GridInput properties(Constant, Hidden=true) + requiredProperties = {'initialPressure'}; + gridFields = kwave.toolbox.GridField.createGridFieldsMap([ kwave.toolbox.GridField('initialPressure', Attributes={'real', 'finite'}) kwave.toolbox.GridField('initialVelocity', Attributes={'real', 'finite'}, Type=kwave.toolbox.GridFieldType.VectorField) ]); + end end diff --git a/+kwave/+toolbox/@ComplexSource/ComplexSource.m b/+kwave/+toolbox/@ComplexSource/ComplexSource.m index cccffe05..fb1501ff 100644 --- a/+kwave/+toolbox/@ComplexSource/ComplexSource.m +++ b/+kwave/+toolbox/@ComplexSource/ComplexSource.m @@ -30,8 +30,24 @@ %% See Also % * |kwave.toolbox.GridInput| +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef ComplexSource < kwave.toolbox.GridInput - + properties(Dependent=true) sourceFieldMagnitude sourceFieldPhase @@ -56,7 +72,7 @@ function sourceFieldPhase = get.sourceFieldPhase(obj) sourceFieldPhase = angle(obj.subsref(struct('type', '.', 'subs', 'sourceField'))); end - + end end diff --git a/+kwave/+toolbox/@ElasticMedium/ElasticMedium.m b/+kwave/+toolbox/@ElasticMedium/ElasticMedium.m index 9450353d..6cb09d92 100644 --- a/+kwave/+toolbox/@ElasticMedium/ElasticMedium.m +++ b/+kwave/+toolbox/@ElasticMedium/ElasticMedium.m @@ -40,6 +40,22 @@ %% See Also % * |GridInput| +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef ElasticMedium < kwave.toolbox.GridInput properties(Constant, Hidden=true) diff --git a/+kwave/+toolbox/@ElasticSource/ElasticSource.m b/+kwave/+toolbox/@ElasticSource/ElasticSource.m new file mode 100644 index 00000000..cccd487d --- /dev/null +++ b/+kwave/+toolbox/@ElasticSource/ElasticSource.m @@ -0,0 +1,52 @@ +%% ElasticSource +% *Package:* kwave.toolbox +% *Superclasses:* kwave.toolbox.GridInput +% +% Class used to define the elastic sources for a simulation. +% +%% Syntax +% source = ElasticSource(kgrid); +% +%% Description +% This class is used to define the elastic source terms. The constructor +% takes an object of the |kwave.toolbox.Grid| class which defines the grid +% size. Source matrices must match the grid size defined by kgrid. +% +%% Examples +% Define the grid and source objects, and assign the initial pressure. +% +% kgrid = kwave.toolbox.Grid([128, 128], 1e-3); +% source = kwave.toolbox.ElasticSource(kgrid); +% source.initialPressure = rand(source.gridSize); +% +%% Properties +% * |initialPressure| - (numeric) Initial pressure distribution [Pa]. +%% See Also +% * |GridInput| + +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + +classdef ElasticSource < kwave.toolbox.GridInput + + properties(Constant, Hidden=true) + requiredProperties = {}; + gridFields = kwave.toolbox.GridField.createGridFieldsMap([ + kwave.toolbox.GridField('initialPressure', Attributes={'real', 'finite'}) + ]); + end + +end diff --git a/+kwave/+toolbox/@ElectromagneticMedium/ElectromagneticMedium.m b/+kwave/+toolbox/@ElectromagneticMedium/ElectromagneticMedium.m deleted file mode 100644 index 6ee6d052..00000000 --- a/+kwave/+toolbox/@ElectromagneticMedium/ElectromagneticMedium.m +++ /dev/null @@ -1,47 +0,0 @@ -%% ElectromagneticMedium -% *Package:* kwave.toolbox -% *Superclasses:* kwave.toolbox.GridInput -% -% Class used to define the electromagnetic medium properties for a -% simulation using Maxwell's equations. -% -%% Syntax -% medium = ElectromagneticMedium(kgrid); -% -%% Description -% This class is used to define the electromagnetic medium properties -% required for a simulation based on Maxwell's equations. The simulation is -% limited to isotropic, linear materials so these must be scalar fields. -% The constructor takes an object of the |kwave.toolbox.Grid| class which -% defines the grid size. -% -%% Examples -% Define the grid and medium objects, and assign the electromagnetic -% properties. -% -% kgrid = kwave.toolbox.Grid([128, 128, 128], 1e-3); -% medium = kwave.toolbox.ElectromagneticMedium(kgrid); -% medium.permittivity = rand(medium.gridSize); -% medium.permeability = rand(medium.gridSize); -% medium.conductivity = rand(medium.gridSize); -% -%% Properties -% * |permittivity| - (isotropic) permittivity [F/m]. -% * |permeability| - (isotropic) permeability [H/m]. -% * |conductivity| - (isotropic) conductivity [S/m]. -% -%% See Also -% * |GridInput| - -classdef ElectromagneticMedium < kwave.toolbox.GridInput - - properties(Constant, Hidden=true) - requiredProperties = {'permittivity','permeability'}; - gridFields = kwave.toolbox.GridField.createGridFieldsMap([ - kwave.toolbox.GridField('permittivity'); - kwave.toolbox.GridField('permeability'); - kwave.toolbox.GridField('conductivity'); - ]); - end - -end diff --git a/+kwave/+toolbox/@ElectromagneticSource/ElectromagneticSource.m b/+kwave/+toolbox/@ElectromagneticSource/ElectromagneticSource.m deleted file mode 100644 index af29605b..00000000 --- a/+kwave/+toolbox/@ElectromagneticSource/ElectromagneticSource.m +++ /dev/null @@ -1,60 +0,0 @@ -%% ElectromagneticSource -% *Package:* kwave.toolbox -% *Superclasses:* kwave.toolbox.GridInput -% -% Class used to define the sources for an electromagnetic simulation. -% -%% Syntax -% source = ElectromagneticSource(kgrid); -% -%% Description -% This class is used to define the source terms to Maxwell's equations. The -% constructor takes an object of the |kwave.toolbox.Grid| class which -% defines the grid size. The first three source dimensions must match the -% grid size defined by kgrid, with the fourth dimension being used for -% vector components. The number of vector components much match -% |kgrid.dimensions|. -% -% When using staggered grids, the components of |initialElectricField| and -% |electricCurrentDensitySource| must be defined on the staggered grid -% given by -% x-component initialElectricField(:,:,:,1) on (0, +1/2, +1/2) -% y-component initialElectricField(:,:,:,2) on (+1/2, 0, +1/2) -% z-component initialElectricField(:,:,:,3) on (+1/2, +1/2, 0) -% -% When using staggered grids, the components of |initialMagneticField| and -% |magneticCurrentDensitySource| must be defined on the staggered grid -% given by -% x-component initialMagneticField(:,:,:,1) on (+1/2, 0, 0) -% y-component initialMagneticField(:,:,:,2) on (0, +1/2, 0) -% z-component initialMagneticField(:,:,:,3) on (0, 0, +1/2) -% -%% Examples -% Define the grid and source objects, and assign an initial electric field. -% -% kgrid = kwave.toolbox.Grid([128, 128], 1e-3); -% source = kwave.toolbox.ElectromagneticSource(kgrid); -% source.initialElectricField = rand([source.gridSize, 2]); -% -%% Properties -% * |initialElectricField| - Initial electric field vector distribution [V/m]. -% * |initialMagneticField| - Initial magnetic field vector distribution [A/m]. -% * |electricCurrentDensitySource| - Electric current density vector source [A/m^2]. -% * |magneticCurrentDensitySource| - Magnetic current density vector source [T]. -% -%% See Also -% * |GridInput| - -classdef ElectromagneticSource < kwave.toolbox.GridInput - - properties(Constant, Hidden=true) - requiredProperties = {}; - gridFields = kwave.toolbox.GridField.createGridFieldsMap([ - kwave.toolbox.GridField('initialElectricField', Attributes={'real', 'finite'}, Type={'VectorField'}) % E field - kwave.toolbox.GridField('initialMagneticField', Attributes={'real', 'finite'}, Type={'VectorField'}) % H field - kwave.toolbox.GridField('electricCurrentDensitySource', Attributes={'real', 'finite'}, Type={'VectorField'}) % J source - kwave.toolbox.GridField('magneticCurrentDensitySource', Attributes={'real', 'finite'}, Type={'VectorField'}) % M source - ]); - end - -end diff --git a/+kwave/+toolbox/@FourierCollocation/FourierCollocation.m b/+kwave/+toolbox/@FourierCollocation/FourierCollocation.m index d3fe9a06..c3064fa5 100644 --- a/+kwave/+toolbox/@FourierCollocation/FourierCollocation.m +++ b/+kwave/+toolbox/@FourierCollocation/FourierCollocation.m @@ -27,23 +27,23 @@ % %% Examples % Calculate the gradient of a scalar field in 2D, and compare with the -% MATLAB || function. +% MATLAB || function. % % % Define input field. % [x, y] = meshgrid(-2:.2:2, -2:.2:2); % z = x .* exp(-x.^2 - y.^2); -% +% % % Define input field. % kgrid = kwave.toolbox.Grid([40, 40], 0.2); % f = kgrid.x .* exp(-kgrid.x.^2 - kgrid.y.^2); -% +% % % Compute gradient using k-Wave. % fourierDiffOps = kwave.toolbox.FourierCollocation(kgrid); % gradf = fourierDiffOps.gradient(f); -% +% % % Compute gradient using finite differences with MATLAB gradient function. % [fy, fx] = gradient(f, kgrid.dx); -% +% % % Plot. % figure; % subplot(2, 3, 1); @@ -51,31 +51,31 @@ % colorbar; % axis image; % title('k-Wave \partialf/\partialx'); -% +% % subplot(2, 3, 2); % imagesc(fx); % colorbar; % axis image; % title('Finite Difference \partialf/\partialx'); -% +% % subplot(2, 3, 3); % imagesc(abs(gradf(:, :, :, 1) - fx)); % colorbar; % axis image; % title('Difference'); -% +% % subplot(2, 3, 4); % imagesc(gradf(:, :, :, 2)); % colorbar; % axis image; % title('k-Wave \partialf/\partialy'); -% +% % subplot(2, 3, 5); % imagesc(fy); % colorbar; % axis image; % title('Finite Difference \partialf/\partialy'); -% +% % subplot(2, 3, 6); % imagesc(abs(gradf(:, :, :, 2) - fy)); % colorbar; @@ -98,28 +98,29 @@ %% Methods % * |divergence| % * |divergenceSplit| +% * |fracLaplacian| % * |gradient| -% * |fractionalLaplacian| -% * |stagger| % * |plotField| % * |sinc| +% * |stagger| -% Copyright (C) 2022- University College London. +% Copyright (C) 2024- The k-Wave Authors. % % This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free % software: you can redistribute it and/or modify it under the terms of the % GNU Lesser General Public License as published by the Free Software % Foundation, either version 3 of the License, or (at your option) any % later version. -% +% % k-Wave-II is distributed in the hope that it will be useful, but WITHOUT % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or % FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public % License for more details. -% +% % You should have received a copy of the GNU Lesser General Public License % along with k-Wave-II. If not, see . + classdef FourierCollocation < handle % Properties set by the constructor. @@ -182,7 +183,7 @@ obj.ddxNoShift = reshape(ifftshift(1i * obj.kgridPadded.kxVec), [], 1, 1); obj.ddyNoShift = reshape(ifftshift(1i * obj.kgridPadded.kyVec), 1, [], 1); obj.ddzNoShift = reshape(ifftshift(1i * obj.kgridPadded.kzVec), 1, 1, []); - + obj.ddxShiftPos = reshape(ifftshift(1i * obj.kgridPadded.kxVec .* exp( 1i.*obj.kgridPadded.kxVec * obj.kgridPadded.dx/2)), [], 1, 1); obj.ddyShiftPos = reshape(ifftshift(1i * obj.kgridPadded.kyVec .* exp( 1i.*obj.kgridPadded.kyVec * obj.kgridPadded.dy/2)), 1, [], 1); obj.ddzShiftPos = reshape(ifftshift(1i * obj.kgridPadded.kzVec .* exp( 1i.*obj.kgridPadded.kzVec * obj.kgridPadded.dz/2)), 1, 1, []); diff --git a/+kwave/+toolbox/@FourierCollocation/curl.m b/+kwave/+toolbox/@FourierCollocation/curl.m index 3a4b9a79..0634f3f4 100644 --- a/+kwave/+toolbox/@FourierCollocation/curl.m +++ b/+kwave/+toolbox/@FourierCollocation/curl.m @@ -10,18 +10,18 @@ % %% Description % Calculates the components of the curl of a vector field in 3D using a -% Fourier collocation spectral method. -% +% Fourier collocation spectral method. +% % The vector components of the input and output fields are stacked in the % 4th dimension. For example, the input must have dimensions (Nx, Ny, Nz, % 3) and the output will be the same size. This is to allow codes to % implement multi-dimensional support by always looping over the fourth -% dimension. +% dimension. % % The curl operations are defined on the padded grid. Thus, the inputs to % this function must also be defined on the padded grid. The output can be % returned on a spatially staggered grid by setting the optional -% |Staggering| argument. +% |Staggering| argument. % %% Input Arguments % * |f| - (numeric) Vector field to compute divergence of. @@ -34,28 +34,29 @@ % % * |Staggering| - ('none', 'forward', 'backward') Option to return the % output staggered by half a grid point in the specified direction. -% Defatult = 'none'. +% Default = 'none'. % %% Output Arguments % * |df| - (numeric) Components of divergence of f. -% Copyright (C) 2022- University College London. +% Copyright (C) 2024- The k-Wave Authors. % % This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free % software: you can redistribute it and/or modify it under the terms of the % GNU Lesser General Public License as published by the Free Software % Foundation, either version 3 of the License, or (at your option) any % later version. -% +% % k-Wave-II is distributed in the hope that it will be useful, but WITHOUT % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or % FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public % License for more details. -% +% % You should have received a copy of the GNU Lesser General Public License % along with k-Wave-II. If not, see . + function f = curl(obj, f, options) % Note: operations can be performed in-place, so we don't allocate a % separate output variable. @@ -68,7 +69,7 @@ % Check the input is a 3D vector field if size(f,4) ~= 3 - kwave.toolbox.Logger.error('FourierCollocation:not3DVectorField', 'curl is only defined for three-dimensional vector fields.'); + kwave.toolbox.Logger.error('FourierCollocation:not3DVectorField', 'curl is only defined for three-dimensional vector fields.'); end % Calculate the gradients of each component of the field. diff --git a/+kwave/+toolbox/@FourierCollocation/divergence.m b/+kwave/+toolbox/@FourierCollocation/divergence.m index 908a739b..1b850bc7 100644 --- a/+kwave/+toolbox/@FourierCollocation/divergence.m +++ b/+kwave/+toolbox/@FourierCollocation/divergence.m @@ -1,4 +1,4 @@ -%% divergence +%% Divergence % *Class:* kwave.toolbox.FourierCollocation % *Package:* kwave.toolbox % @@ -11,15 +11,15 @@ %% Description % Calculates the divergence of a vector field in 1D, 2D, or 3D using a % Fourier collocation spectral method. -% +% % The vector components of the input field are stacked in the 4th % dimension. For example, if calling divergence on a vector field in 2D, % the input should have dimensions (Nx, Ny, 1, 2), while the output will % have size (Nx, Ny). This is to allow codes to implement multi-dimensional % support by always looping over the fourth dimension. % -% If obj.kappa is defined, a k-space correction is applied as part of the -% divergence calulation. If kappa is a scalar (single frequency correction) +% If |obj.kappa| is defined, a k-space correction is applied as part of the +% divergence calculation. If kappa is a scalar (single frequency correction) % or empty, the contributions to the divergence for each of the Cartesian % coordinates are calculated using 1D FFTs. If kappa is a matrix, they are % calculated using ND FFTs, and kappa is applied in the Fourier domain. @@ -40,27 +40,28 @@ % % * |Staggering| - ('none', 'forward', 'backward') Option to return the % output staggered by half a grid point in the specified direction. -% Defatult = 'none'. +% Default = 'none'. % %% Output Arguments % * |df| - (numeric) Divergence of f. -% Copyright (C) 2022- University College London. +% Copyright (C) 2024- The k-Wave Authors. % % This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free % software: you can redistribute it and/or modify it under the terms of the % GNU Lesser General Public License as published by the Free Software % Foundation, either version 3 of the License, or (at your option) any % later version. -% +% % k-Wave-II is distributed in the hope that it will be useful, but WITHOUT % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or % FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public % License for more details. -% +% % You should have received a copy of the GNU Lesser General Public License % along with k-Wave-II. If not, see . + function f = divergence(obj, f, varargin) % Calculate components of divergence, and then sum them. Devolve input diff --git a/+kwave/+toolbox/@FourierCollocation/divergenceSplit.m b/+kwave/+toolbox/@FourierCollocation/divergenceSplit.m index 5ea10d8c..7d57f1df 100644 --- a/+kwave/+toolbox/@FourierCollocation/divergenceSplit.m +++ b/+kwave/+toolbox/@FourierCollocation/divergenceSplit.m @@ -1,4 +1,4 @@ -%% divergenceSplit +%% Divergence Split % *Class:* kwave.toolbox.FourierCollocation % *Package:* kwave.toolbox % @@ -12,7 +12,7 @@ % Calculates the split components (e.g., dfx/dx, dfy/dy) of the divergence % of a vector field in 1D, 2D, or 3D using a Fourier collocation spectral % method. This is equivalent to the diagonal of the vector gradient. -% +% % The vector components of the input field, and the split components of the % divergence, are stacked in the 4th dimension. For example, if calling % divergence on a vector field in 2D, the input should have dimensions (Nx, @@ -20,8 +20,8 @@ % to implement multi-dimensional support by always looping over the fourth % dimension. % -% If obj.kappa is defined, a k-space correction is applied as part of the -% divergence calulation. If kappa is a scalar (single frequency correction) +% If |obj.kappa| is defined, a k-space correction is applied as part of the +% divergence calculation. If kappa is a scalar (single frequency correction) % or empty, the contributions to the divergence for each of the Cartesian % coordinates are calculated using 1D FFTs. If kappa is a matrix, they are % calculated using ND FFTs, and kappa is applied in the Fourier domain. @@ -42,27 +42,28 @@ % % * |Staggering| - ('none', 'forward', 'backward') Option to return the % output staggered by half a grid point in the specified direction. -% Defatult = 'none'. +% Default = 'none'. % %% Output Arguments % * |df| - (numeric) Components of divergence of f. -% Copyright (C) 2022- University College London. +% Copyright (C) 2024- The k-Wave Authors. % % This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free % software: you can redistribute it and/or modify it under the terms of the % GNU Lesser General Public License as published by the Free Software % Foundation, either version 3 of the License, or (at your option) any % later version. -% +% % k-Wave-II is distributed in the hope that it will be useful, but WITHOUT % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or % FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public % License for more details. -% +% % You should have received a copy of the GNU Lesser General Public License % along with k-Wave-II. If not, see . + function f = divergenceSplit(obj, f, options) % Note: operations can be performed in-place, so we don't allocate a % separate output variable. diff --git a/+kwave/+toolbox/@FourierCollocation/fracLaplacian.m b/+kwave/+toolbox/@FourierCollocation/fracLaplacian.m index 2417dc83..9f70dc0c 100644 --- a/+kwave/+toolbox/@FourierCollocation/fracLaplacian.m +++ b/+kwave/+toolbox/@FourierCollocation/fracLaplacian.m @@ -1,61 +1,61 @@ -%% Update ================================================================= -% -% %% fracLaplacian -% % *Class:* kwave.toolbox.FourierCollocation -% % *Package:* kwave.toolbox -% % -% % Calculate fractionLaplacian of scalar field for a given power. -% % -% %% Syntax -% % ddf = fracLaplacian(obj, f,y) -% % ddf = fracLaplacian(obj, f,y,Staggering='forward') -% % -% %% Description -% % Calculates the gradient of a scalar field in 1D, 2D, or 3D using a -% % Fourier collocation spectral method. -% % -% % The vector components of the gradient are stacked in the 4th dimension of -% % the output. For example, if calling gradient on a matrix of dimensions -% % (10, 10), the output will be of size (10, 10, 1, 2). This is to allow -% % codes to implement multi-dimensional support by always looping over the -% % fourth dimension. -% % -% % If y==0, the function returns the original function. -% % If y==2, the function returns the traditional laplacaian. -% % -% %% Input Arguments -% % * |f| - (numeric) Scalar field to compute gradient of. -% % * |y| - (numeric) scalar value for power of fractional Laplacian -% % -% %% Name-Value Arguments -% % Specify optional pairs of arguments as |Name1=Value1,...,NameN=ValueN|, -% % where |Name| is the argument name and |Value| is the corresponding value. -% % Name-value arguments must appear after other arguments, but the order of -% % the pairs does not matter. -% % -% % * |Staggering| - ('none', 'forward', 'backward') Option to return the -% % output staggered by half a grid point in the specified direction. -% % Defatult = 'none'. -% % -% %% Output Arguments -% % * |df| - (numeric) Fractional Laplacian of f. -% -% % Copyright (C) 2024- The k-Wave-II Authors. -% % -% % This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free -% % software: you can redistribute it and/or modify it under the terms of the -% % GNU Lesser General Public License as published by the Free Software -% % Foundation, either version 3 of the License, or (at your option) any -% % later version. -% % -% % k-Wave-II is distributed in the hope that it will be useful, but WITHOUT -% % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -% % FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public -% % License for more details. -% % -% % You should have received a copy of the GNU Lesser General Public License -% % along with k-Wave-II. If not, see . -%% ======================================================================== +%% Fractional Laplacian +% *Class:* kwave.toolbox.FourierCollocation +% *Package:* kwave.toolbox +% +% Calculate fractionLaplacian of scalar field for a given power. +% +%% Syntax +% ddf = fracLaplacian(obj, f,y) +% ddf = fracLaplacian(obj, f,y,Staggering='forward') +% +%% Description +% Calculates the gradient of a scalar field in 1D, 2D, or 3D using a +% Fourier collocation spectral method. +% +% The vector components of the gradient are stacked in the 4th dimension of +% the output. For example, if calling gradient on a matrix of dimensions +% (10, 10), the output will be of size (10, 10, 1, 2). This is to allow +% codes to implement multi-dimensional support by always looping over the +% fourth dimension. +% +% If y==0, the function returns the original function. +% If y==2, the function returns the traditional laplacian. +% +%% Input Arguments +% * |f| - (numeric) Scalar field to compute gradient of. +% * |y| - (numeric) scalar value for power of fractional Laplacian +% +%% Name-Value Arguments +% Specify optional pairs of arguments as |Name1=Value1,...,NameN=ValueN|, +% where |Name| is the argument name and |Value| is the corresponding value. +% Name-value arguments must appear after other arguments, but the order of +% the pairs does not matter. +% +% * |Staggering| - ('none', 'forward', 'backward') Option to return the +% output staggered by half a grid point in the specified direction. +% Default = 'none'. +% +%% Output Arguments +% * |df| - (numeric) Fractional Laplacian of f. + +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + + + function ddf = fracLaplacian(obj, f,y, options) %Computes (-Nab^2)^y through the Fourier Collication method @@ -88,5 +88,3 @@ % ND k-space derivative ddf = ifftn(dLap .*xshift.*yshift.*zshift.* fftn(f), 'symmetric'); - - diff --git a/+kwave/+toolbox/@FourierCollocation/gradient.m b/+kwave/+toolbox/@FourierCollocation/gradient.m index 235c39dc..c57b5def 100644 --- a/+kwave/+toolbox/@FourierCollocation/gradient.m +++ b/+kwave/+toolbox/@FourierCollocation/gradient.m @@ -1,4 +1,4 @@ -%% gradient +%% The Gradient % *Class:* kwave.toolbox.FourierCollocation % *Package:* kwave.toolbox % @@ -11,19 +11,19 @@ %% Description % Calculates the gradient of a scalar field in 1D, 2D, or 3D using a % Fourier collocation spectral method. -% +% % The vector components of the gradient are stacked in the 4th dimension of % the output. For example, if calling gradient on a matrix of dimensions % (10, 10), the output will be of size (10, 10, 1, 2). This is to allow % codes to implement multi-dimensional support by always looping over the % fourth dimension. % -% If obj.kappa is defined, a k-space correction is applied as part of the -% gradient calulation. If kappa is a scalar (single frequency correction) +% If |obj.kappa| is defined, a k-space correction is applied as part of the +% gradient calculation. If kappa is a scalar (single frequency correction) % or empty, the gradient components are calculated using 1D FFTs. If kappa % is a matrix, the gradient components are calculated using ND FFTs, and % kappa is applied in the Fourier domain. -% +% % The gradient operations (and kappa if defined) are defined on the padded % grid. Thus, the inputs to this function must also be defined on the % padded grid. The output can be returned on a spatially staggered grid by @@ -40,27 +40,28 @@ % % * |Staggering| - ('none', 'forward', 'backward') Option to return the % output staggered by half a grid point in the specified direction. -% Defatult = 'none'. +% Default = 'none'. % %% Output Arguments % * |df| - (numeric) Gradient of f. -% Copyright (C) 2022- University College London. +% Copyright (C) 2024- The k-Wave Authors. % % This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free % software: you can redistribute it and/or modify it under the terms of the % GNU Lesser General Public License as published by the Free Software % Foundation, either version 3 of the License, or (at your option) any % later version. -% +% % k-Wave-II is distributed in the hope that it will be useful, but WITHOUT % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or % FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public % License for more details. -% +% % You should have received a copy of the GNU Lesser General Public License % along with k-Wave-II. If not, see . + function df = gradient(obj, f, options) arguments diff --git a/+kwave/+toolbox/@FourierCollocation/plotField.m b/+kwave/+toolbox/@FourierCollocation/plotField.m index 674da099..517d129f 100644 --- a/+kwave/+toolbox/@FourierCollocation/plotField.m +++ b/+kwave/+toolbox/@FourierCollocation/plotField.m @@ -1,4 +1,4 @@ -%% plotField +%% Plot Field % *Class:* kwave.toolbox.FourierCollocation % *Package:* kwave.toolbox % @@ -10,29 +10,31 @@ %% Description % Plots the provided field using the plot settings defined in the % |kwave.toolbox.Settings| object. For 1D and 2D inputs, the complete field -% is plotted using || and ||, respectively. For 3D inputs, the three orthogonal planes +% is plotted using || +% and ||, +% respectively. For 3D inputs, the three orthogonal planes % through the origin are plotted. % %% Input Arguments % * |f| - (numeric) Field to plot. -% Copyright (C) 2022- University College London. +% Copyright (C) 2024- The k-Wave Authors. % % This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free % software: you can redistribute it and/or modify it under the terms of the % GNU Lesser General Public License as published by the Free Software % Foundation, either version 3 of the License, or (at your option) any % later version. -% +% % k-Wave-II is distributed in the hope that it will be useful, but WITHOUT % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or % FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public % License for more details. -% +% % You should have received a copy of the GNU Lesser General Public License % along with k-Wave-II. If not, see . + function plotField(obj, f) switch obj.dimensions @@ -51,13 +53,13 @@ function plotField(obj, f) imagesc(obj.kgrid.yVec, obj.kgrid.xVec, squeeze(f(:, :, round(end/2))), obj.settings.plotScale); title('x-y plane (central slice)'); axis image; - + subplot(2, 2, 2); imagesc(obj.kgrid.zVec, obj.kgrid.xVec, squeeze(f(:, round(end/2), :)), obj.settings.plotScale); title('x-z plane (central slice)'); axis image; xlabel(['(All axes in ' 'm)']); - + subplot(2, 2, 3); imagesc(obj.kgrid.zVec, obj.kgrid.yVec, squeeze(f(round(end/2), :, :)), obj.settings.plotScale); title('y-z plane (central slice)'); diff --git a/+kwave/+toolbox/@FourierCollocation/sinc.m b/+kwave/+toolbox/@FourierCollocation/sinc.m index 299cb8bd..bd5300a1 100644 --- a/+kwave/+toolbox/@FourierCollocation/sinc.m +++ b/+kwave/+toolbox/@FourierCollocation/sinc.m @@ -1,4 +1,4 @@ -%% sinc +%% The Sinc Function % *Class:* kwave.toolbox.FourierCollocation % *Package:* kwave.toolbox % @@ -9,7 +9,7 @@ % %% Description % Computes y = sin(x)/x. Differs from MATLAB's internal -% || function which computes y = sin(pi*x)/(pi*x). +% || function which computes y = sin(pi*x)/(pi*x). % %% Input Arguments % * |x| - (numeric) Input. @@ -17,22 +17,23 @@ %% Output Arguments % * |y| - (numeric) sin(x)/x. -% Copyright (C) 2022- University College London. +% Copyright (C) 2024- The k-Wave Authors. % % This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free % software: you can redistribute it and/or modify it under the terms of the % GNU Lesser General Public License as published by the Free Software % Foundation, either version 3 of the License, or (at your option) any % later version. -% +% % k-Wave-II is distributed in the hope that it will be useful, but WITHOUT % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or % FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public % License for more details. -% +% % You should have received a copy of the GNU Lesser General Public License % along with k-Wave-II. If not, see . + function y = sinc(x) zero_vals = (x == 0); diff --git a/+kwave/+toolbox/@FourierCollocation/stagger.m b/+kwave/+toolbox/@FourierCollocation/stagger.m index bcd3fc3c..cdd87700 100644 --- a/+kwave/+toolbox/@FourierCollocation/stagger.m +++ b/+kwave/+toolbox/@FourierCollocation/stagger.m @@ -1,4 +1,4 @@ -%% stagger +%% The Stagger Operation % *Class:* kwave.toolbox.FourierCollocation % *Package:* kwave.toolbox % @@ -11,17 +11,17 @@ %% Description % Calculates the staggered function of a scalar field in 1D, 2D, or 3D using a % Fourier method, staggering in each grid spacing individually. -% +% % The vector components of the staggered field are stacked in the 4th dimension of -% the output. Staggering only in one dimension each. +% the output. Staggering only in one dimension each. % For example, if calling stagger on a matrix of dimensions % (10, 10), the output will be of size (10, 10, 1, 2). This is to allow % codes to implement multi-dimensional support by always looping over the % fourth dimension. -% -% The stagger operations are defined on the padded grid. Thus, the inputs -% to this function must also be defined on the padded grid. The output can -% be returned on a spatially staggered grid by setting the optional |Staggering| +% +% The stagger operations are defined on the padded grid. Thus, the inputs +% to this function must also be defined on the padded grid. The output can +% be returned on a spatially staggered grid by setting the optional |Staggering| % argument. % %% Input Arguments @@ -35,13 +35,13 @@ % % * |Staggering| - ('forward', 'backward') Option to return the % output staggered by half a grid point in the specified direction. -% Defatult = 'forward'. +% Default = 'forward'. % * |Type| - ('fourier', 'linInterpolate') % %% Output Arguments -% * |f| - (numeric) f but staggered in each co-ordinate direction. +% * |f| - (numeric) f but staggered in each coordinate direction. -% Copyright (C) 2024- University College London. +% Copyright (C) 2024- The k-Wave Authors. % % This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free % software: you can redistribute it and/or modify it under the terms of the @@ -57,6 +57,7 @@ % You should have received a copy of the GNU Lesser General Public License % along with k-Wave-II. If not, see . + function fstg = stagger(obj, f, options) arguments @@ -85,7 +86,7 @@ f_k = fftn(f); for dimInd = 1:obj.dimensions switch dimInd - case 1 + case 1 fstg(:, :, :, 1) = ifftn(xshift .* f_k, 'symmetric'); case 2 fstg(:, :, :, 2) = ifftn(yshift .* f_k, 'symmetric'); diff --git a/+kwave/+toolbox/@Grid/Grid.m b/+kwave/+toolbox/@Grid/Grid.m index 2cc88fec..8c8a236c 100644 --- a/+kwave/+toolbox/@Grid/Grid.m +++ b/+kwave/+toolbox/@Grid/Grid.m @@ -11,7 +11,7 @@ % |Grid| is the grid class used across the k-Wave Toolbox. An object % of the |Grid| class defines the grid coordinates and spatial % frequency (wavevector) matrices for a particular simulation. -% +% % The grid is assumed to be a regular Cartesian grid with grid spacing % given by |gridSpacing|. The spatial grid matrices are indexed as: (x, 1) % in 1D; (x, y) in 2D; and (x, y, z) in 3D. Typically the grid spacing in @@ -91,21 +91,38 @@ %% Methods % * |assignWithGridPadding| % * |displayGridSize| +% * |getWavenumbers| % * |highestPrimeFactors| % * |returnWithoutGridPadding| % * |validateSize| +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef Grid < handle - + % Properties set by constructor. properties(SetAccess=immutable) - + % Number of dimensions. - dimensions(1,1) double {mustBeLessThanOrEqual(dimensions,3)} = 0; + dimensions(1,1) double {mustBeLessThanOrEqual(dimensions,3)} = 0; % Grid size [grid points]. gridSize(1,3) double {mustBeInteger, mustBePositive, mustBeFinite} = [1, 1, 1]; - + % Grid point spacing [m]. gridSpacing(1,3) double {mustBeNonnegative, mustBeFinite} = [0, 0, 0]; @@ -113,16 +130,16 @@ gridPadding(1,3) double {mustBeInteger, mustBeNonnegative, mustBeFinite} = [0, 0, 0]; end - + % Dependent properties without set methods. These parameters are not % stored but re-computed each time they are needed. properties(Dependent=true, GetAccess=public, SetAccess=private) - + % Grid size in each direction [grid points]. Nx; Ny; Nz; - + % Grid point spacing in each direction [m]. dx; dy; @@ -131,19 +148,19 @@ % 1D vector of grid coordinates [m]. xVec; yVec; - zVec; - + zVec; + % Nx by Ny by Nz matrix containing repeated copies of the grid % coordinates [m]. x; y; z; - + % Physical size of grid [m]. xSize; ySize; zSize; - + % Total number of grid points. totalGridPoints; @@ -162,7 +179,7 @@ kxMax; kyMax; kzMax; - + % Nx by Ny by Nz matrix of scalar wavenumber [rad/m]. k; @@ -170,11 +187,11 @@ kMax; end - + % Constructor. methods function obj = Grid(gridSize, gridSpacing, gridPadding) - + % Set grid dimensions based on length of gridSize vector. obj.dimensions = numel(gridSize); @@ -195,10 +212,10 @@ end end - + % Get methods for dependent properties. methods - + % Grid size and spacing in each direction. function Nx = get.Nx(obj) Nx = obj.gridSize(1); @@ -284,7 +301,7 @@ function zSize = get.zSize(obj) zSize = obj.Nz .* obj.dz; end - + % Total number of grid points. function N = get.totalGridPoints(obj) N = prod(obj.gridSize); @@ -344,7 +361,7 @@ case 3 ky = repmat(obj.kyVec.', [obj.Nx, 1, obj.Nz]); end - end + end function kz = get.kz(obj) switch obj.dimensions @@ -356,7 +373,7 @@ kz = repmat(permute(obj.kzVec, [2 3 1]), [obj.Nx, obj.Ny, 1]); end end - + % Maximum supported frequency. function kxMax = get.kxMax(obj) kxMax = max(abs(obj.kxVec(:))); @@ -375,9 +392,9 @@ kMaxVec(kMaxVec == 0) = []; kMax = min(kMaxVec); end - + end - + % General class methods. methods highestPrimeFactors = highestPrimeFactors(obj); @@ -386,9 +403,9 @@ matrix = assignWithGridPadding(obj, matrix, edgeValues); matrix = returnWithoutGridPadding(obj, matrix); end - + % Methods that can only be accessed by class members. - methods (Access='protected', Static=true, Hidden=true) + methods (Access='protected', Static=true, Hidden=true) kVec = getWavenumbers(Nx, dx) end end diff --git a/+kwave/+toolbox/@Grid/assignWithGridPadding.m b/+kwave/+toolbox/@Grid/assignWithGridPadding.m index 250ecb56..2433af59 100644 --- a/+kwave/+toolbox/@Grid/assignWithGridPadding.m +++ b/+kwave/+toolbox/@Grid/assignWithGridPadding.m @@ -1,4 +1,4 @@ -%% assignWithGridPadding +%% Assign With Grid Padding % *Class:* kwave.toolbox.Grid % *Package:* kwave.toolbox % @@ -24,6 +24,22 @@ %% See Also % * |kwave.toolbox.Grid.returnWithoutGridPadding| +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + function paddedMatrix = assignWithGridPadding(obj, matrix, expansionValue) arguments diff --git a/+kwave/+toolbox/@Grid/displayGridSize.m b/+kwave/+toolbox/@Grid/displayGridSize.m index 920d43f1..e7312134 100644 --- a/+kwave/+toolbox/@Grid/displayGridSize.m +++ b/+kwave/+toolbox/@Grid/displayGridSize.m @@ -1,4 +1,4 @@ -%% displayGridSize +%% Display Grid Size % *Class:* kwave.toolbox.Grid % *Package:* kwave.toolbox % @@ -9,7 +9,23 @@ % %% Description % Displays the input and computational grid sizes using -% kwave.toolbox.Logger.info. +% |kwave.toolbox.Logger.info|. + +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . function displayGridSize(obj) @@ -45,7 +61,7 @@ function printGridSize(gridString, gridSize, gridSpacing, dimensions) num2str(gridSize(3)) ' grid points (' ... num2str(gridSizeScaled(1)) ' by ' ... num2str(gridSizeScaled(2)) ' by ' ... - num2str(gridSizeScaled(3)) prefix 'm)']); + num2str(gridSizeScaled(3)) prefix 'm)']); end end diff --git a/+kwave/+toolbox/@Grid/getWavenumbers.m b/+kwave/+toolbox/@Grid/getWavenumbers.m index f682e944..77452260 100644 --- a/+kwave/+toolbox/@Grid/getWavenumbers.m +++ b/+kwave/+toolbox/@Grid/getWavenumbers.m @@ -1,4 +1,4 @@ -%% getWavenumbers +%% Get Wavenumbers % *Class:* kwave.toolbox.Grid % *Package:* kwave.toolbox % @@ -11,7 +11,7 @@ % Creates the vector of wavenumbers (spatial frequencies) for use with the % MATLAB fft functions. Internally, MATLAB uses FFTW, so the frequency bins % match those used by FFTW. The DC component is returned in the centre of -% the spectrum. Use || to transform this +% the spectrum. Use || to transform this % so the DC component is the first element. % %% Input Arguments @@ -21,6 +21,22 @@ %% Output Arguments % * |kVec| - (double) Wavenumber vector. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + function kVec = getWavenumbers(numGridPoints, gridSpacing) arguments @@ -53,4 +69,4 @@ nx(floor(numGridPoints/2) + 1) = 0; % Define the wavenumber vector components. -kVec = (2*pi/gridSpacing) .* nx; +kVec = (2*pi/gridSpacing) .* nx; diff --git a/+kwave/+toolbox/@Grid/highestPrimeFactors.m b/+kwave/+toolbox/@Grid/highestPrimeFactors.m index 8b2e275b..5a1a76dd 100644 --- a/+kwave/+toolbox/@Grid/highestPrimeFactors.m +++ b/+kwave/+toolbox/@Grid/highestPrimeFactors.m @@ -1,4 +1,4 @@ -%% highestPrimeFactors +%% Highest Prime Factors % *Class:* kwave.toolbox.Grid % *Package:* kwave.toolbox % @@ -15,14 +15,29 @@ % % kgrid = kwave.toolbox.Grid([32, 31], 1e-3); % kgrid.highestPrimeFactors -% +% % ans = -% % 2 31 1 % %% Output Arguments % * |primeFacs| - (numeric) Highest prime factor in [x, y, z]. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + function primeFacs = highestPrimeFactors(obj) primeFacs = [max(factor(obj.Nx)), max(factor(obj.Ny)), max(factor(obj.Nz))]; diff --git a/+kwave/+toolbox/@Grid/returnWithoutGridPadding.m b/+kwave/+toolbox/@Grid/returnWithoutGridPadding.m index 5f811d49..e50a7c2e 100644 --- a/+kwave/+toolbox/@Grid/returnWithoutGridPadding.m +++ b/+kwave/+toolbox/@Grid/returnWithoutGridPadding.m @@ -1,4 +1,4 @@ -%% returnWithoutGridPadding +%% Return Without Grid Padding % *Class:* kwave.toolbox.Grid % *Package:* kwave.toolbox % @@ -20,6 +20,22 @@ %% See Also % * |kwave.toolbox.Grid.assignWithGridPadding| +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + function matrix = returnWithoutGridPadding(obj, matrix) arguments diff --git a/+kwave/+toolbox/@Grid/validateSize.m b/+kwave/+toolbox/@Grid/validateSize.m index ab4678ed..0b95555e 100644 --- a/+kwave/+toolbox/@Grid/validateSize.m +++ b/+kwave/+toolbox/@Grid/validateSize.m @@ -1,4 +1,4 @@ -%% validateSize +%% Validate Size % *Class:* kwave.toolbox.Grid % *Package:* kwave.toolbox % @@ -10,7 +10,7 @@ % %% Description % Checks if the size of an input matrix matches the |gridSize| property of -% a |Grid| object using . By default, the grid size not including padding is % used. To include padding, set |IncludePadding=true|. If the input matrix % is a scalar, the check is skipped. @@ -49,6 +49,22 @@ % * |VariableName| - (char) Name of the matrix variable. Used to add % information to any error message thrown. Default = ''. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + function validateSize(obj, matrix, options) arguments @@ -82,7 +98,7 @@ function validateSize(obj, matrix, options) end validateattributes(matrix, ... - {'numeric'}, ... + {'numeric','logical'}, ... {'size', expectedGridSize}, ... options.FunctionName, ... options.VariableName); diff --git a/+kwave/+toolbox/@GridField/GridField.m b/+kwave/+toolbox/@GridField/GridField.m index e04bc087..36794dac 100644 --- a/+kwave/+toolbox/@GridField/GridField.m +++ b/+kwave/+toolbox/@GridField/GridField.m @@ -17,9 +17,9 @@ % the pairs does not matter. % % * |Classes| - (cell array) Valid data types passed to -% . +% . % * |Attributes| - (cell array) Valid attributes passed to -% . +% . % * |ExpansionValue| - (cell array) Scalar value to use in the matrix % expansion passed to |kwave.toolbox.expandMatrix|. % @@ -37,6 +37,22 @@ % * |createGridFieldsMap| - Static utility method to define a map for grid % field properties. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef GridField < handle properties diff --git a/+kwave/+toolbox/@GridField/createGridFieldsMap.m b/+kwave/+toolbox/@GridField/createGridFieldsMap.m index 704c0e0b..716bc22f 100644 --- a/+kwave/+toolbox/@GridField/createGridFieldsMap.m +++ b/+kwave/+toolbox/@GridField/createGridFieldsMap.m @@ -1,4 +1,4 @@ -%% createGridFieldsMap +%% Create Grid Fields Map % *Class:* kwave.toolbox.GridField % *Package:* kwave.toolbox % @@ -10,7 +10,7 @@ % %% Description % The |createGridFieldsMap| method is designed to create a -% || that provides handling +% || that provides handling % and validation details for grid field properties. Each key of the map % corresponds to a grid field property's name, and the associated value is % a |kwave.toolbox.GridField| object. This utility is used to generate the @@ -25,6 +25,22 @@ % * |fieldsMap| - (|containers.Map|) A map where the keys are the property % names and the values are |kwave.toolbox.GridField| +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + function fieldsMap = createGridFieldsMap(fieldDefinitions) fieldsMap = containers.Map; diff --git a/+kwave/+toolbox/@GridFieldType/GridFieldType.m b/+kwave/+toolbox/@GridFieldType/GridFieldType.m index 4228cbdb..d6f53334 100644 --- a/+kwave/+toolbox/@GridFieldType/GridFieldType.m +++ b/+kwave/+toolbox/@GridFieldType/GridFieldType.m @@ -19,6 +19,22 @@ % * |kwave.toolbox.GridField| % * |kwave.toolbox.GridInput| +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef GridFieldType enumeration ScalarField diff --git a/+kwave/+toolbox/@GridInput/GridInput.m b/+kwave/+toolbox/@GridInput/GridInput.m index 0280edb7..9720552d 100644 --- a/+kwave/+toolbox/@GridInput/GridInput.m +++ b/+kwave/+toolbox/@GridInput/GridInput.m @@ -23,6 +23,7 @@ % simpler and more intuitive interface to the user. % % Features of the class include: +% % * Dynamic property management: This class uses dynamic properties % to efficiently handle grid field properties, automatically taking care % of grid padding. @@ -44,7 +45,7 @@ % % obj.(propertyName) % -% where propertyName is a char holding the property name. This ensures +% where |propertyName| is a char holding the property name. This ensures % consistent access to the properties, considering the custom behaviors % introduced by the overloaded subsref method. % @@ -84,15 +85,13 @@ % medium.myProperty = rand(medium.gridSize); % medium.checkRequiredProperties; % size(medium.myProperty) -% +% % ans = -% % 128 128 -% +% % size(medium.myPropertyPadded) -% +% % ans = -% % 148 148 % %% Input Arguments @@ -103,7 +102,7 @@ % * |kgrid| - (kwave.toolbox.Grid) Handle for |Grid| object. % * |gridSize| - (double) Number of grid points in each Cartesian direction % [grid points]. Convenience property that returns kgrid.gridSize. -% * |requiredProperties| - (cell array) List of properties that must be +% * |requiredProperties| - (cell array) List of properties that must be % set for the simulation. Defined in derived classes. % * |gridFields| - (containers.Map) Map containing a % |kwave.toolbox.GridField| for each virtual property. @@ -120,6 +119,22 @@ % * |subsref| - Overloaded method for accessing properties. It returns the % properties defined in |gridFields| without padding. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef(Abstract) GridInput < dynamicprops & matlab.mixin.CustomDisplay % Properties set by constructor. diff --git a/+kwave/+toolbox/@GridInput/checkRequiredProperties.m b/+kwave/+toolbox/@GridInput/checkRequiredProperties.m index 11070557..4888c644 100644 --- a/+kwave/+toolbox/@GridInput/checkRequiredProperties.m +++ b/+kwave/+toolbox/@GridInput/checkRequiredProperties.m @@ -1,4 +1,4 @@ -%% checkRequiredProperties +%% Check Required Properties % *Class:* kwave.toolbox.GridInput % *Package:* kwave.toolbox % @@ -21,6 +21,22 @@ % |kwave.toolbox.GridInput| class. This object's |requiredProperties| % attribute determines which properties must be checked. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + function checkRequiredProperties(obj) for ind = 1:length(obj.requiredProperties) diff --git a/+kwave/+toolbox/@GridInput/getPropertyGroups.m b/+kwave/+toolbox/@GridInput/getPropertyGroups.m index e966ffaa..52a3c65c 100644 --- a/+kwave/+toolbox/@GridInput/getPropertyGroups.m +++ b/+kwave/+toolbox/@GridInput/getPropertyGroups.m @@ -1,8 +1,8 @@ -%% getPropertyGroups +%% Get Property Groups % *Class:* kwave.toolbox.GridInput % *Package:* kwave.toolbox % -% Overloads the matlab.mixin.CustomDisplay method to return a property +% Overloads the |matlab.mixin.CustomDisplay| method to return a property % group for enhanced display in the MATLAB command window. % %% Syntax @@ -29,6 +29,22 @@ % that represents the structured view of the object's properties, % suitable for display in the MATLAB command window. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + function propgrp = getPropertyGroups(obj) propertyStruct = struct(); diff --git a/+kwave/+toolbox/@GridInput/subsasgn.m b/+kwave/+toolbox/@GridInput/subsasgn.m index 25387d31..ea267f6a 100644 --- a/+kwave/+toolbox/@GridInput/subsasgn.m +++ b/+kwave/+toolbox/@GridInput/subsasgn.m @@ -1,4 +1,4 @@ -%% subsasgn +%% The subsasgn Method % *Class:* kwave.toolbox.GridInput % *Package:* kwave.toolbox % @@ -24,7 +24,7 @@ %% Input Arguments % * |obj| - (kwave.toolbox.GridInput) Instance of the GridInput class. % * |S| - (struct) MATLAB structure specifying the target property and type -% of assignment. +% of assignment. % * |value| - (various) The data to be assigned. Its attributes are % validated if the target is a virtual property. % @@ -33,6 +33,22 @@ % updated with the new assignment, whether it was to a virtual or a % standard property. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + function obj = subsasgn(obj, S, value) if (S(1).type == '.') && (obj.gridFields.isKey(S(1).subs)) @@ -40,7 +56,7 @@ % If there's more than a single assignment, it means that something is % being done to the referenced variable. So we first do it and then % assign the value. - if length(S) > 1 + if length(S) > 1 unpaddedArray = obj.subsref(S(1)); S2 = S(2:end); value = builtin('subsasgn', unpaddedArray, S2, value); diff --git a/+kwave/+toolbox/@GridInput/subsref.m b/+kwave/+toolbox/@GridInput/subsref.m index 8b0daa2d..a0aad594 100644 --- a/+kwave/+toolbox/@GridInput/subsref.m +++ b/+kwave/+toolbox/@GridInput/subsref.m @@ -1,4 +1,4 @@ -%% subsref +%% The subsref method % *Class:* kwave.toolbox.GridInput % *Package:* kwave.toolbox % @@ -21,13 +21,29 @@ %% Input Arguments % * |obj| - (kwave.toolbox.GridInput) Instance of the GridInput class. % * |S| - (struct) MATLAB structure specifying the target property or -% method and type of referencing. +% method and type of referencing. % %% Output Arguments % * |value| - (various) The retrieved data or method output. If the target % is a virtual property, it will be the non-padded representation of the % underlying data. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + function varargout = subsref(obj, S) if (S(1).type == '.') && (obj.gridFields.isKey(S(1).subs)) diff --git a/+kwave/+toolbox/@LogLevels/LogLevels.m b/+kwave/+toolbox/@LogLevels/LogLevels.m index 99512006..e14a2859 100644 --- a/+kwave/+toolbox/@LogLevels/LogLevels.m +++ b/+kwave/+toolbox/@LogLevels/LogLevels.m @@ -30,6 +30,22 @@ %% See Also % * |kwave.toolbox.Logger| +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef LogLevels < int8 enumeration Debug(0) diff --git a/+kwave/+toolbox/@Logger/Logger.m b/+kwave/+toolbox/@Logger/Logger.m index 6f136f46..83cd0824 100644 --- a/+kwave/+toolbox/@Logger/Logger.m +++ b/+kwave/+toolbox/@Logger/Logger.m @@ -85,18 +85,34 @@ % shown alongside the log message. Defaults to false. % %% See Also -% * || -% * || +% * || +% * || % * |kwave.toolbox.LogLevels| +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef Logger < handle - + properties (Access = private) logLevel (1,1) kwave.toolbox.LogLevels; logFile char; displayTimeStamp (1,1) logical end - + methods (Access = private) % Constructor. @@ -105,7 +121,7 @@ obj.logFile = ''; obj.displayTimeStamp = false; end - + % Method to log messages with different log levels. function logMessage(obj, level, message) if level >= obj.logLevel @@ -113,7 +129,7 @@ function logMessage(obj, level, message) formattedMessage = message; if obj.displayTimeStamp timestamp = datetime('now', 'Format', 'yyyy-MM-dd HH:mm:ss'); - formattedMessage = sprintf('%s - %s: %s', timestamp, char(level), formattedMessage); + formattedMessage = sprintf('%s - %s: %s', timestamp, char(level), formattedMessage); end if isempty(obj.logFile) @@ -128,7 +144,7 @@ function logMessage(obj, level, message) end end - + methods (Static, Hidden=true) % Static method to access the singleton instance. @@ -144,13 +160,13 @@ function logMessage(obj, level, message) end singleInstance = instance; end - + % Static method to log messages. function log(level, message) logger = kwave.toolbox.Logger.getInstance(); logger.logMessage(level, message); end - + end methods (Static) @@ -158,20 +174,20 @@ function log(level, message) % Method to reset the Logger instance. function reset() kwave.toolbox.Logger.getInstance(true); - end + end % Static method to set the log level. function setLogLevel(level) logger = kwave.toolbox.Logger.getInstance(); logger.logLevel = level; end - + % Static method to set the output to a file. function setLogToFile(fileName) logger = kwave.toolbox.Logger.getInstance(); logger.logFile = fileName; end - + % Static method to set the output to the command window. function setLogToCommandLine() logger = kwave.toolbox.Logger.getInstance(); @@ -183,23 +199,23 @@ function showTimeStamp(value) logger = kwave.toolbox.Logger.getInstance(); logger.displayTimeStamp = value; end - + % Static method to log debug messages. function debug(message) kwave.toolbox.Logger.log(kwave.toolbox.LogLevels.Debug, message); end - + % Static method to log info messages. function info(message) kwave.toolbox.Logger.log(kwave.toolbox.LogLevels.Info, message); end - + % Static method to log warning messages and call warning. function warning(message) kwave.toolbox.Logger.log(kwave.toolbox.LogLevels.Warning, message); warning(message); end - + % Static method to log error messages and throw an exception. function error(errID, errorMessage) kwave.toolbox.Logger.log(kwave.toolbox.LogLevels.Error, errorMessage); diff --git a/+kwave/+toolbox/@Materials/Materials.m b/+kwave/+toolbox/@Materials/Materials.m new file mode 100644 index 00000000..7ecc4a25 --- /dev/null +++ b/+kwave/+toolbox/@Materials/Materials.m @@ -0,0 +1,297 @@ +%% Materials +% *Package:* kwave.toolbox +% +% Class for holding material property structures so that materials can be +% referenced using a single index. Used by the Medium class. Initialised with +% some default materials, but additional materials can be added. Each +% material structure within the Materials object must have at least the +% fields: +% +% * soundSpeed [m/s] +% * density [kg/m^3] +% +% Optionally, a material may also have: +% +% * absorption coefficient prefactor (alpha0) [dB/cm/MHz^y] +% * absorption power law exponent (y) +% * nonlinearity parameter B/A (BonA) +% * specific heat capacity (C) [J/kg/K] +% * thermal conductivity (k_cond) [W/m/K] +% +% Index handling: +% +% * The 'index' field is optional when adding a material. +% * If omitted, addMaterial auto-assigns the lowest free uint8 index in [0..255]. +% * If provided, it must be a unique uint8-compatible integer in [0..255]. +% +% Examples +% materials = Materials(); +% T = materials.listMaterials() +% I = materials.listMaterialIndices() +% [materials, idx] = materials.addMaterial('softTissue', struct('soundSpeed',1540,'density',1000)); +% materials.addMaterial('softTissue', struct('index',10,'soundSpeed',1540,'density',1000)); +% +% See Also +% +% * |Medium| + +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + +classdef Materials < dynamicprops + + properties (Constant, Access = private) + REQUIRED_FIELDS = {'soundSpeed','density'}; + OPTIONAL_FIELDS = { + 'index', ... + 'absorptionCoeff',... + 'absorptionPower',... + 'BonA', ... + 'specificHeat',... + 'thermalConductivity'}; + + FIELD_VALIDATORS = struct( ... + 'soundSpeed', @kwave.toolbox.Materials.validateNonNegScalar, ... + 'density', @kwave.toolbox.Materials.validateNonNegScalar, ... + 'absorptionCoeff', @kwave.toolbox.Materials.validateNonNegScalar, ... + 'absorptionPower', @kwave.toolbox.Materials.validateNonNegScalar, ... + 'BonA', @kwave.toolbox.Materials.validateNonNegScalar, ... + 'specificHeat', @kwave.toolbox.Materials.validateNonNegScalar, ... + 'thermalConductivity', @kwave.toolbox.Materials.validateNonNegScalar, ... + 'index', @kwave.toolbox.Materials.validateIndexField ... + ); + + RESERVED_PROPS = {'REQUIRED_FIELDS','OPTIONAL_FIELDS','FIELD_VALIDATORS','RESERVED_PROPS'}; + end + + + % Methods for validating Materials fields attributes + methods (Static, Access = private) + + function out = validateNonNegScalar(value, fieldName) + try + mustBeNumeric(value); + mustBeReal(value); + mustBeFinite(value); + mustBeNonnegative(value); + catch + error('Materials:NotNonNegativeScalar', ... + ['Field "%s" must be a non-negative real scalar.', fieldName]); + end + + % Enforce single + out = single(value); + + end + + function out = validateIndexField(value, fieldName) + if ~(isnumeric(value) && isscalar(value) && ... + value >= 0 && value <= 255 && value == floor(value)) + error('Materials:InvalidIndexField',... + ['Field "%s" must be an integer scalar in the range [0, 255].', fieldName]); + end + + % Enforce uint8 type + out = uint8(value); + end + + end + + methods + + function obj = Materials() + + % Pre-populate with two examples (types are enforced on add). + water = struct( ... + 'index', 0, ... % uint8 limits the number of materials to 256 + 'soundSpeed', 1480, ... % m/s @ ~20°C + 'density', 1000, ... % kg/m^3 + 'absorptionCoeff', 0.5, ... % dB/cm/MHz^y + 'absorptionPower', 2, ... % (y) dimensionless + 'BonA', 5, ... % dimensionless + 'specificHeat', 4181, ... % J/(kg·K) + 'thermalConductivity', 0.58 ); % W/(m·K) + obj.addMaterial('water', water); + + air = struct( ... + 'index', 1, ... + 'soundSpeed', 343, ... + 'density', 1.225, ... + 'BonA', 0.7, ... + 'specificHeat', 1005, ... + 'thermalConductivity', 0.026 ); + obj.addMaterial('air', air); + + end + + function assignedIdx = addMaterial(obj, name, data) + % ADDMATERIAL Add a new material to the Materials object. + % assignedIdx = addMaterial(obj, name, data) adds a new material with the + % given name and structure. If the structure does not contain an 'index' + % field, the next available index in [0,255] is assigned. Returns the + % assigned index value. + + % Validate that 'name' is a string-like scalar + if ~( (ischar(name) && isrow(name)) || (isstring(name) && isscalar(name)) ) + error('Materials:InvalidNameType', ... + 'Material name must be a char row vector or a string scalar.'); + end + + % Normalize to char for addprop + name = char(name); + + % Check material name is not reserved + if ismember(name, obj.RESERVED_PROPS) + error('Materials:ReservedName', ... + '"%s" is a reserved property name and cannot be used.', name); + end + + % Check the name does not already exist on this object + if isprop(obj, name) + error('Materials:NameExists', ... + 'A material or property named "%s" already exists.', name); + end + + % Check required fields + for f = obj.REQUIRED_FIELDS + if ~isfield(data, f{1}) + error('Materials:MissingField', ... + 'Material "%s" is missing required field "%s".', name, f{1}); + end + end + + % Check for unknown fields + allowed = [obj.REQUIRED_FIELDS, obj.OPTIONAL_FIELDS]; + dataFields = fieldnames(data); + for f = dataFields' + if ~ismember(f{1}, allowed) + error('Materials:UnknownField', ... + 'Unknown field "%s" in material "%s".', f{1}, name); + end + end + + % Collect existing indices + materialNames = properties(obj); + usedIdx = []; + for m = materialNames' + matStruct = obj.(m{1}); + if isstruct(matStruct) && isfield(matStruct, "index") + usedIdx(end+1) = double(matStruct.index); %#ok + end + end + + % Handle index: validate if provided, else allocate + if isfield(data, "index") + % Validate/normalize the provided index first (to uint8, in-range, integer) + data.index = obj.FIELD_VALIDATORS.index(data.index, 'index'); + + % Check for collision + if ismember(double(data.index), usedIdx) + error('Materials:IndexInUse', ... + 'Index %d is already used by another material.', double(data.index)); + end + + assignedIdx = data.index; % already uint8 + else + % Find smallest unused index in [0,255] + candidateIdx = 0; + while ismember(candidateIdx, usedIdx) + candidateIdx = candidateIdx + 1; + if candidateIdx > 255 + error('Materials:NoIndicesLeft', ... + 'No available indices left in range [0,255].'); + end + end + + data.index = uint8(candidateIdx); + assignedIdx = data.index; + end + + % Validate and convert each field + dataFields = fieldnames(data); + for f = dataFields' + fname = f{1}; + if isfield(obj.FIELD_VALIDATORS, fname) + data.(fname) = obj.FIELD_VALIDATORS.(fname)(data.(fname), fname); + end + end + + % Add dynamic property and store the struct + obj.addprop(name); + obj.(name) = data; + + end + + function T = listMaterials(obj) + % LISTMATERIALS Return a table of all materials and their standard properties, + % sorted by ascending index. + [names, idxCol] = obj.getMaterialNamesAndIndicesSorted(); + + % Fixed order for display (index is shown as a separate column) + orderedFields = {'soundSpeed','density', ... + 'absorptionCoeff','absorptionPower','BonA', ... + 'specificHeat','thermalConductivity'}; + + n = numel(names); + data = NaN(n, numel(orderedFields), 'single'); + + for i = 1:n + s = obj.(names{i}); + + % Other fields + for j = 1:numel(orderedFields) + f = orderedFields{j}; + if isfield(s, f) + data(i, j) = single(s.(f)); + end + end + end + + varNames = [{'Name','Index'}, orderedFields]; + T = table( string(names(:)), double(idxCol(:)), ... + data(:,1), data(:,2), data(:,3), data(:,4), data(:,5), data(:,6), data(:,7), ... + 'VariableNames', varNames); + + end + + function I = listMaterialIndices(obj) + % LISTMATERIALINDICES Return Name-Index mapping as a table, + % sorted by ascending index. + [names, idx] = obj.getMaterialNamesAndIndicesSorted(); + I = table( string(names(:)), idx(:), 'VariableNames', {'Name','Index'} ); + end + end + + methods (Access = private) + function names = getMaterialNames(obj) + % GETMATERIALNAMES Return only dynamic material property names. + allProps = properties(obj); + names = setdiff(allProps, obj.RESERVED_PROPS, 'stable'); + + end + + function [namesSorted, idxSorted] = getMaterialNamesAndIndicesSorted(obj) + %GETMATERIALNAMESANDINDICESSORTED Return material names and indices sorted by index asc. + names = obj.getMaterialNames(); + idx = zeros(numel(names),1,'uint8'); + for i = 1:numel(names) + idx(i) = obj.(names{i}).index; + end + [idxSorted, order] = sort(idx, 'ascend'); + namesSorted = names(order); + end + end +end diff --git a/+kwave/+toolbox/@Medium/BonA.m b/+kwave/+toolbox/@Medium/BonA.m deleted file mode 100644 index 0601e77b..00000000 --- a/+kwave/+toolbox/@Medium/BonA.m +++ /dev/null @@ -1,50 +0,0 @@ -%% BonA -% *Class:* kwave.toolbox.Medium -% *Package:* kwave.toolbox -% -% Calls the BonA across the spatial grid. -% -%% Syntax -% BonA(obj) -% -%% Description -% recalls the BonA spatial distribution according to the materialIDGrid, -% and the MaterialTable given as part of the medium. this is performed by -% generating the padded Density and removing the PML. -% -%% See Also -% * |BonAPadded| - -% Copyright (C) 2025- The k-Wave Authors. -% -% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free -% software: you can redistribute it and/or modify it under the terms of the -% GNU Lesser General Public License as published by the Free Software -% Foundation, either version 3 of the License, or (at your option) any -% later version. -% -% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT -% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public -% License for more details. -% -% You should have received a copy of the GNU Lesser General Public License -% along with k-Wave-II. If not, see . - -function sS=BonA(obj) -arguments - obj -end -if ~isscalar(obj.BonAPadded) - V=obj.BonAPadded; - switch obj.kgrid.dimensions - case 1 - sS=V(obj.kgrid.gridPadding(1)+1:end-obj.kgrid.gridPadding(1)); - case 2 - sS=V(obj.kgrid.gridPadding(1)+1:end-obj.kgrid.gridPadding(1), obj.kgrid.gridPadding(2)+1:end-obj.kgrid.gridPadding(2)); - case 3 - sS=V(obj.kgrid.gridPadding(1)+1:end-obj.kgrid.gridPadding(1), obj.kgrid.gridPadding(2)+1:end-obj.kgrid.gridPadding(2), obj.kgrid.gridPadding(3)+1:end-obj.kgrid.gridPadding(3)); - end -else - sS=obj.BonAPadded; -end \ No newline at end of file diff --git a/+kwave/+toolbox/@Medium/BonAPadded.m b/+kwave/+toolbox/@Medium/BonAPadded.m deleted file mode 100644 index 3f871935..00000000 --- a/+kwave/+toolbox/@Medium/BonAPadded.m +++ /dev/null @@ -1,43 +0,0 @@ -%% BonAPadded -% *Class:* kwave.toolbox.Medium -% *Package:* kwave.toolbox -% -% Calls the BonA value across the padded spatial grid. -% -%% Syntax -% BonAPadded(obj) -% -%% Description -% recalls the BonA spatial distribution according to the materialIDGrid, -% using the generated materialIDGridPadded, and the given MaterialTable -% from the medium. -% -% This method has been generated to allow interactions with the acoustic -% solver class, and the acousticMedium grid field property BonA (BonAPadded) - -% Copyright (C) 2025- The k-Wave Authors. -% -% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free -% software: you can redistribute it and/or modify it under the terms of the -% GNU Lesser General Public License as published by the Free Software -% Foundation, either version 3 of the License, or (at your option) any -% later version. -% -% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT -% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public -% License for more details. -% -% You should have received a copy of the GNU Lesser General Public License -% along with k-Wave-II. If not, see . - -function sS=BonAPadded(obj) -arguments - obj -end -if ~isscalar(obj.materialIDGridPadded) - sS=reshape(obj.materialTable(obj.materialIDGridPadded,5),size(obj.materialIDGridPadded)); -else - sS=obj.materialTable(obj.materialIDGridPadded,5); -end -end \ No newline at end of file diff --git a/+kwave/+toolbox/@Medium/Medium.m b/+kwave/+toolbox/@Medium/Medium.m index 1969e4ec..2d4847b5 100644 --- a/+kwave/+toolbox/@Medium/Medium.m +++ b/+kwave/+toolbox/@Medium/Medium.m @@ -2,78 +2,313 @@ % *Package:* kwave.toolbox % *Superclasses:* kwave.toolbox.GridInput % -% Class used to define the acoustic medium properties for a simulation. +% Class used to define the spatial distribution of material types using +% indices from a |kwave.toolbox.Materials| object, and a grid from a +% |kwave.toolbox.Grid| object. % %% Syntax -% medium = Medium(kgrid); +% medium = Medium(kgrid, materials); % %% Description -% This class is used to define the acoustic medium properties. The -% constructor takes an object of the |kwave.toolbox.Grid| class which -% defines the grid size. All medium properties can be scalar or have the -% same size as the grid with the exception of |alphaPower| and -% |soundSpeedReference|, which must be scalar. The |soundSpeed| and -% |density| must be defined. -% -%% Examples -% Define the grid and medium objects, and assign the sound speed and -% density. -% -% kgrid = kwave.toolbox.Grid([128, 128], 1e-3); -% medium = kwave.toolbox.AcousticMedium(kgrid); -% medium.soundSpeed = rand(medium.gridSize); -% medium.density = rand(medium.gridSize); -% medium.specificHeat = rand(medium.gridSize); -% medium.thermalConductivity = rand(medium.gridSize); +% The |Medium| class defines the material properties by specifying, at each +% grid point, an integer material identifier. +% +% All material properties, sound speed, density, etc. are stored in a +% separate |kwave.toolbox.Materials| object. Each material in a |Materials| +% object is assigned a unique uint8 index in the range [0, 255]. +% +% The |materialIndexGrid| property is a uint8 array with the same size as +% the computational grid defined by the input |kwave.toolbox.Grid| object. +% Each element specifies which material is present at that point. The +% Medium object maps these material indices to the corresponding +% material properties using the lookup table in the |Materials| object. +% +% All grid-valued medium properties (listed below) are registered as +% *GridInput* virtual properties. This means: +% * Reading |medium.prop| returns the **unpadded** map. +% * A hidden **padded** backing store |medium.propPadded| is created +% automatically and used internally by solvers when required. % %% Properties -% * |soundSpeed| - (single) Compressional sound speed [m/s]. -% * |soundSpeedReference| - (single) Reference compressional sound speed -% used in the k-space correction [m/s]. Automatically defined in -% kWaveAcoustic if not defined by the user. -% * |density| - (single) Mass density [kg/m^2]. -% * |absorptionCoeff| - (single) Power law attenuation coefficient -% [dB/(MHz^y cm)]. -% * |absorptionPower| - (single) Power law attenuation power. -% * |BonA| - (single) Parameter of nonlinearity. -% * |diffusionReference| - (numeric) Reference diffusion coefficient used -% in the k-space correction term [m^2/s]. Automatically defined in -% ThermalSolver if not defined by the user. -% * |specificHeat| - (numeric) Specific heat capacity at constant pressure -% [J/kg/K]. -% * |thermalConductivity| - (numeric) Thermal conductivity [W/m/K]. +% +% * |materialIndexGrid| — A uint8 array with the same size as the grid. +% Each element stores a material index referring to an entry in the +% associated |kwave.toolbox.Materials| object. +% +% Grid-valued virtual properties (unpadded by default, padded versions +% available internally as |...Padded|): +% +% * |soundSpeed| — [m/s] +% * |density| — [kg/m^3] +% * |absorptionCoeff| — power‑law absorption prefactor [dB/cm/MHz^y] +% * |absorptionPowerMap| — power‑law exponent y [dimensionless] +% * |BonA| — nonlinearity parameter B/A [dimensionless] +% * |specificHeat| — specific heat capacity [J/(kg·K)] +% * |thermalConductivity| — thermal conductivity [W/(m·K)] +% +% Scalar properties (user-settable; default computed on first map build): +% +% * |referenceSoundSpeed| — scalar. Default = max(soundSpeed(:)) +% * |referenceDiffusion| — scalar. Default = max(thermalConductivity ./ ... +% (density .* specificHeat)) +% +% These are set automatically the first time the derived maps are built +% (e.g., when |materialIndexGrid| is first assigned). If the user later +% assigns a value to either scalar, that value is preserved on subsequent +% updates to the maps (i.e., not auto-overwritten). +% +% Note: The acoustic solver uses the single scalar value |absorptionPower| +% at all voxels (algorithmic constraint). +% %% See Also -% * |GridInput| +% * |Grid| +% * |Materials| + +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . classdef Medium < kwave.toolbox.GridInput + % Medium class with one user-set virtual property (materialIndexGrid), + % and a set of read-only derived grid-valued virtual properties whose + % padded counterparts (propPadded) are managed via GridInput. + + % Configuration (constants) + properties(Constant, Hidden=true) + % One user-assignable virtual property + requiredProperties = {'materialIndexGrid'}; + + % All virtual grid fields (user-assignable + derived read-only) + gridFields = kwave.toolbox.GridField.createGridFieldsMap([ ... + kwave.toolbox.GridField('materialIndexGrid', ... + Classes={'uint8'}, ... + Attributes={'nonnegative'}, ... + Type=kwave.toolbox.GridFieldType.ScalarField), ... + kwave.toolbox.GridField('soundSpeed', ... + Classes={'single'}, Attributes={'real'}, ... + Type=kwave.toolbox.GridFieldType.ScalarField), ... + kwave.toolbox.GridField('density', ... + Classes={'single'}, Attributes={'real'}, ... + Type=kwave.toolbox.GridFieldType.ScalarField), ... + kwave.toolbox.GridField('absorptionCoeff', ... + Classes={'single'}, Attributes={'real'}, ... + Type=kwave.toolbox.GridFieldType.ScalarField), ... + kwave.toolbox.GridField('absorptionPowerMap', ... + Classes={'single'}, Attributes={'real'}, ... + Type=kwave.toolbox.GridFieldType.ScalarField), ... + kwave.toolbox.GridField('BonA', ... + Classes={'single'}, Attributes={'real'}, ... + Type=kwave.toolbox.GridFieldType.ScalarField), ... + kwave.toolbox.GridField('specificHeat', ... + Classes={'single'}, Attributes={'real'}, ... + Type=kwave.toolbox.GridFieldType.ScalarField), ... + kwave.toolbox.GridField('thermalConductivity', ... + Classes={'single'}, Attributes={'real'}, ... + Type=kwave.toolbox.GridFieldType.ScalarField) ... + ]); + DERIVED_GRID_KEYS = { ... + 'soundSpeed','density','absorptionCoeff','absorptionPowerMap', ... + 'BonA','specificHeat','thermalConductivity' ... + }; + end + + properties(Dependent=true, SetAccess=private) + absorptionPower + end + + properties(SetAccess=immutable, Hidden=true) + materials(1,1) kwave.toolbox.Materials + end + + % Scalars (user-settable) properties - soundSpeedReference single {mustBeReal, mustBePositive, mustBeFinite} - diffusionReference {mustBeReal, mustBeFinite} - materialTable (:,7) single {mustBeReal, mustBeNonnegative, mustBeFinite} = kwave.toolbox.BuildMaterialTable() + soundSpeedReference (1,1) double {mustBeNonnegative, mustBeFinite} + diffusionReference (1,1) double {mustBeNonnegative, mustBeFinite} end - properties(Constant, Hidden=false) - requiredProperties = {'materialIDGrid'}; %'soundSpeed', 'density', 'specificHeat', 'thermalConductivity'}; - gridFields = kwave.toolbox.GridField.createGridFieldsMap([ - kwave.toolbox.GridField('materialIDGrid') + % Constructor + methods + function obj = Medium(kgrid, materials) + arguments + kgrid(1,1) kwave.toolbox.Grid + materials(1,1) kwave.toolbox.Materials + end + obj@kwave.toolbox.GridInput(kgrid); + obj.materials = materials; + end + end - ]); + + % Override assignment + methods + function obj = subsasgn(obj, S, value) + % Make derived grid properties read-only; only materialIndexGrid + % (and non-virtual/base properties) can be set by the user. + if (S(1).type == '.') && ischar(S(1).subs) + propName = S(1).subs; + + % Block assignment to derived maps + if ismember(propName, obj.DERIVED_GRID_KEYS) + error('Medium:ReadOnlyProperty', ... + 'The property "%s" is read-only and cannot be assigned.', propName); + end + + % Intercept assignments to materialIndexGrid to refresh maps + if strcmp(propName, 'materialIndexGrid') + + % Delegate to GridInput to perform validation + padding + obj = subsasgn@kwave.toolbox.GridInput(obj, S, value); + % Recompute all derived maps and write their padded backing stores + obj = obj.updateDerivedPaddedMaps(); + return + end + end + + % Fallback to the superclass handling + obj = subsasgn@kwave.toolbox.GridInput(obj, S, value); + end + end + + % Public refresh utility + methods + function obj = refresh(obj) + % Force a rebuild of all derived padded maps (e.g., after Materials changes). + obj = obj.updateDerivedPaddedMaps(); + end end + % Setters methods - soundSpeed = soundSpeedPadded(obj) - density = densityPadded(obj) - absorptionCoeff = absorptionCoeffPadded(obj) - absorptionPower = absorptionPower(obj) - BonA = BonAPadded(obj) - specificHeat = specificHeatPadded(obj) - thermalConductivity = thermalConductivityPadded(obj) - soundSpeed = soundSpeed(obj) - density = density(obj) - absorptionCoeff = absorptionCoeff(obj) - BonA = BonA(obj) - specificHeat = specificHeat(obj) - thermalConductivity = thermalConductivity(obj) + function set.soundSpeedReference(obj, v) + if ~isempty(v) + validateattributes(v, {'double'}, {'scalar','finite','nonnegative'}); + end + obj.soundSpeedReference = v; + end + function set.diffusionReference(obj, v) + if ~isempty(v) + validateattributes(v, {'double'}, {'scalar','finite','nonnegative'}); + end + obj.diffusionReference = v; + end + end + + % Private helpers + methods (Access = private) + + function obj = updateDerivedPaddedMaps(obj) + % Guard: if indices aren't set yet, clear padded maps and return + idx = obj.subsref(struct('type','.', 'subs','materialIndexGrid')); + if isempty(idx) + for i = 1:numel(obj.DERIVED_GRID_KEYS) + obj.( [obj.DERIVED_GRID_KEYS{i} 'Padded'] ) = []; + end + return + end + + % name → field mapping (one place to edit) + mapList = { + 'soundSpeed', 'soundSpeed' + 'density', 'density' + 'absorptionCoeff', 'absorptionCoeff' + 'absorptionPowerMap', 'absorptionPower' % special: map uses 'absorptionPower' + 'BonA', 'BonA' + 'specificHeat', 'specificHeat' + 'thermalConductivity', 'thermalConductivity' + }; + + % Recompute each derived map and write to its padded backing + for k = 1:size(mapList,1) + propName = mapList{k,1}; + fieldName = mapList{k,2}; + unpadded = obj.mapProperty(fieldName); + paddedName = [propName 'Padded']; + obj.(paddedName) = obj.kgrid.assignWithGridPadding(unpadded); + end + + % Only set defaults if the user hasn't assigned them (i.e., still empty) + if obj.soundSpeedReference == 0 + ss = obj.subsref(struct('type','.', 'subs','soundSpeed')); + if ~isempty(ss) + obj.soundSpeedReference = double(max(ss(~isnan(ss)))); + end + end + + if obj.diffusionReference == 0 + tc = obj.subsref(struct('type','.', 'subs','thermalConductivity')); + rho = obj.subsref(struct('type','.', 'subs','density')); + cp = obj.subsref(struct('type','.', 'subs','specificHeat')); + if ~(any(isnan(tc(:))) || any(isnan(rho(:))) || any(isnan(cp(:)))) + denom = rho .* cp; + D = tc ./ denom; + obj.diffusionReference = max(D(:)); + end + end + end + + function vals = mapProperty(obj, fieldName) + % MAPPROPERTY Build an unpadded grid-sized map (single) for the given field. + % Missing properties remain NaN in the result. + + % get the (unpadded) material index grid (uint8 expected) + idx = obj.subsref(struct('type','.', 'subs','materialIndexGrid')); % uint8, unpadded + + % Ensure idx is grid-sized (expand homogeneous scalar) + if isscalar(idx) + idx = repmat(idx, obj.gridSize); + end + + % Look-up table: index 0..255 (uint8) => position 1..256 + lut = NaN(256, 1, 'single'); + + % Name/Index table + T = obj.materials.listMaterialIndices(); % table with variables: Name (string), Index (uint8) + names = T.Name; + indices = T.Index; + + % Populate LUT only for materials that have the requested field + for k = 1:numel(indices) + s = obj.materials.(char(names(k))); % guaranteed: struct with 'index' + if isfield(s, fieldName) + v = s.(fieldName); + if isnumeric(v) && isscalar(v) + lut(double(indices(k)) + 1) = single(v); + end + end + end + + % Map indices to values + vals = lut(double(idx) + 1); + end + end + + % Scalar dependent getters + methods + function v = get.absorptionPower(obj) + + apm = obj.subsref(struct('type','.', 'subs','absorptionPowerMap')); + % Consider an all-zero map with no material-defined values as "unset" + if isempty(apm) || (~any(~isnan(apm(:))) && ~any(apm(:)~=0)) + v = NaN; return + end + vals = apm(~isnan(apm)); + [u,~,idx] = unique(vals); + counts = accumarray(idx,1); + [~,imax] = max(counts); + v = u(imax); + end end end diff --git a/+kwave/+toolbox/@Medium/absorptionCoeff.m b/+kwave/+toolbox/@Medium/absorptionCoeff.m deleted file mode 100644 index 422e04da..00000000 --- a/+kwave/+toolbox/@Medium/absorptionCoeff.m +++ /dev/null @@ -1,50 +0,0 @@ -%% absorptionCoeff -% *Class:* kwave.toolbox.Medium -% *Package:* kwave.toolbox -% -% Calls the absorptionCoeff across the spatial grid. -% -%% Syntax -% absorptionCoeff(obj) -% -%% Description -% recalls the absorption coefficient alpha_0 spatial distribution according to the materialIDGrid, -% and the MaterialTable given as part of the medium. this is performed by -% generating the padded Density and removing the PML. -% -%% See Also -% * |absorptionCoeffPadded| - -% Copyright (C) 2025- The k-Wave Authors. -% -% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free -% software: you can redistribute it and/or modify it under the terms of the -% GNU Lesser General Public License as published by the Free Software -% Foundation, either version 3 of the License, or (at your option) any -% later version. -% -% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT -% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public -% License for more details. -% -% You should have received a copy of the GNU Lesser General Public License -% along with k-Wave-II. If not, see . - -function sS=absorptionCoeff(obj) -arguments - obj -end -if ~isscalar(obj.absorptionCoeffPadded) - V=obj.absorptionCoeffPadded; - switch obj.kgrid.dimensions - case 1 - sS=V(obj.kgrid.gridPadding(1)+1:end-obj.kgrid.gridPadding(1)); - case 2 - sS=V(obj.kgrid.gridPadding(1)+1:end-obj.kgrid.gridPadding(1), obj.kgrid.gridPadding(2)+1:end-obj.kgrid.gridPadding(2)); - case 3 - sS=V(obj.kgrid.gridPadding(1)+1:end-obj.kgrid.gridPadding(1), obj.kgrid.gridPadding(2)+1:end-obj.kgrid.gridPadding(2), obj.kgrid.gridPadding(3)+1:end-obj.kgrid.gridPadding(3)); - end -else - sS=obj.absorptionCoeffPadded; -end \ No newline at end of file diff --git a/+kwave/+toolbox/@Medium/absorptionCoeffPadded.m b/+kwave/+toolbox/@Medium/absorptionCoeffPadded.m deleted file mode 100644 index ba058c5d..00000000 --- a/+kwave/+toolbox/@Medium/absorptionCoeffPadded.m +++ /dev/null @@ -1,44 +0,0 @@ -%% absorptionCoeffPadded -% *Class:* kwave.toolbox.Medium -% *Package:* kwave.toolbox -% -% Calls the absorptionCoeff value across the padded spatial grid. -% -%% Syntax -% absorptionCoeffPadded(obj) -% -%% Description -% recalls the absorptionCoeff spatial distribution according to the materialIDGrid, -% using the generated materialIDGridPadded, and the given MaterialTable -% from the medium. -% -% This method has been generated to allow interactions with the acoustic -% solver class, and the acousticMedium grid field property absorptionCoeff (absorptionCoeffPadded) - -% Copyright (C) 2025- The k-Wave Authors. -% -% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free -% software: you can redistribute it and/or modify it under the terms of the -% GNU Lesser General Public License as published by the Free Software -% Foundation, either version 3 of the License, or (at your option) any -% later version. -% -% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT -% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public -% License for more details. -% -% You should have received a copy of the GNU Lesser General Public License -% along with k-Wave-II. If not, see . - - -function sS=absorptionCoeffPadded(obj) -arguments - obj -end -if ~isscalar(obj.materialIDGridPadded) - sS=reshape(obj.materialTable(obj.materialIDGridPadded,3),size(obj.materialIDGridPadded)); -else - sS=obj.materialTable(obj.materialIDGridPadded,3); -end -end \ No newline at end of file diff --git a/+kwave/+toolbox/@Medium/absorptionPower.m b/+kwave/+toolbox/@Medium/absorptionPower.m deleted file mode 100644 index ccffa2ec..00000000 --- a/+kwave/+toolbox/@Medium/absorptionPower.m +++ /dev/null @@ -1,61 +0,0 @@ -%% absorptionPower -% *Class:* kwave.toolbox.Medium -% *Package:* kwave.toolbox -% -% Sets and Calls the abosrptionPower scalar variable. -% -%% Syntax -% absorptionPower(obj) -% -%% Description -% sets and recalls the absorption Power y spatial distribution according to the materialIDGrid, -% and the MaterialTable given as part of the medium. -% -% This is performed by generating the padded Density and removing the PML. -% The resulting values are then averaged across the grid to produce a -% scalar value, this is by the arithmatic mean. The absorption coefficients -% are not homogonised like this. -% -% Alternative averaging including averaging omega^y for a fixed central -% frequency acros the grid, or according to the total absortion alpha = -% alpha_0 omega^y such that higher absorbing regions are weighted higher. - -% Copyright (C) 2025- The k-Wave Authors. -% -% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free -% software: you can redistribute it and/or modify it under the terms of the -% GNU Lesser General Public License as published by the Free Software -% Foundation, either version 3 of the License, or (at your option) any -% later version. -% -% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT -% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public -% License for more details. -% -% You should have received a copy of the GNU Lesser General Public License -% along with k-Wave-II. If not, see . - -function sS=absorptionPower(obj) -arguments - obj -end -if ~isscalar(obj.materialIDGridPadded) - - disp('absorptionPower value must be scalar, setting to averaged') - V=obj.materialIDGridPadded; - - switch obj.kgrid.dimensions - case 1 - V2=V(obj.kgrid.gridPadding(1)+1:end-obj.kgrid.gridPadding(1)); - case 2 - V2=V(obj.kgrid.gridPadding(1)+1:end-obj.kgrid.gridPadding(1), obj.kgrid.gridPadding(2)+1:end-obj.kgrid.gridPadding(2)); - case 3 - V2=V(obj.kgrid.gridPadding(1)+1:end-obj.kgrid.gridPadding(1), obj.kgrid.gridPadding(2)+1:end-obj.kgrid.gridPadding(2), obj.kgrid.gridPadding(3)+1:end-obj.kgrid.gridPadding(3)); - end - - sS=mean(obj.materialTable(V2,4)); - -else - sS=obj.materialTable(obj.materialIDGridPadded,4); -end \ No newline at end of file diff --git a/+kwave/+toolbox/@Medium/density.m b/+kwave/+toolbox/@Medium/density.m deleted file mode 100644 index 8523a583..00000000 --- a/+kwave/+toolbox/@Medium/density.m +++ /dev/null @@ -1,50 +0,0 @@ -%% density -% *Class:* kwave.toolbox.Medium -% *Package:* kwave.toolbox -% -% Calls the density across the spatial grid. -% -%% Syntax -% density(obj) -% -%% Description -% recalls the density spatial distribution according to the materialIDGrid, -% and the MaterialTable given as part of the medium. this is performed by -% generating the padded Density and removing the PML. -% -%% See Also -% * |densityPadded| - -% Copyright (C) 2025- The k-Wave Authors. -% -% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free -% software: you can redistribute it and/or modify it under the terms of the -% GNU Lesser General Public License as published by the Free Software -% Foundation, either version 3 of the License, or (at your option) any -% later version. -% -% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT -% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public -% License for more details. -% -% You should have received a copy of the GNU Lesser General Public License -% along with k-Wave-II. If not, see . - -function sS=density(obj) -arguments - obj -end -if ~isscalar(obj.densityPadded) - V=obj.densityPadded; - switch obj.kgrid.dimensions - case 1 - sS=V(obj.kgrid.gridPadding(1)+1:end-obj.kgrid.gridPadding(1)); - case 2 - sS=V(obj.kgrid.gridPadding(1)+1:end-obj.kgrid.gridPadding(1), obj.kgrid.gridPadding(2)+1:end-obj.kgrid.gridPadding(2)); - case 3 - sS=V(obj.kgrid.gridPadding(1)+1:end-obj.kgrid.gridPadding(1), obj.kgrid.gridPadding(2)+1:end-obj.kgrid.gridPadding(2), obj.kgrid.gridPadding(3)+1:end-obj.kgrid.gridPadding(3)); - end -else - sS=obj.densityPadded; -end \ No newline at end of file diff --git a/+kwave/+toolbox/@Medium/densityPadded.m b/+kwave/+toolbox/@Medium/densityPadded.m deleted file mode 100644 index 4830fdc1..00000000 --- a/+kwave/+toolbox/@Medium/densityPadded.m +++ /dev/null @@ -1,46 +0,0 @@ -%% densityPadded -% *Class:* kwave.toolbox.Medium -% *Package:* kwave.toolbox -% -% Calls the density value across the padded spatial grid. -% -%% Syntax -% densityPadded(obj) -% -%% Description -% recalls the density spatial distribution according to the materialIDGrid, -% using the generated materialIDGridPadded, and the given MaterialTable -% from the medium. -% -% This method has been generated to allow interactions with the acoustic -% solver class, and the acousticMedium grid field property density (densityPadded) -% as well as with the thermalSolver class and the thermalMedium gridfield -% property density (densityPadded) - -% Copyright (C) 2025- The k-Wave Authors. -% -% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free -% software: you can redistribute it and/or modify it under the terms of the -% GNU Lesser General Public License as published by the Free Software -% Foundation, either version 3 of the License, or (at your option) any -% later version. -% -% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT -% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public -% License for more details. -% -% You should have received a copy of the GNU Lesser General Public License -% along with k-Wave-II. If not, see . - - -function sS=densityPadded(obj) -arguments - obj -end -if ~isscalar(obj.materialIDGridPadded) - sS=reshape(obj.materialTable(obj.materialIDGridPadded,2),size(obj.materialIDGridPadded)); -else - sS=obj.materialTable(obj.materialIDGridPadded,2); -end -end \ No newline at end of file diff --git a/+kwave/+toolbox/@Medium/soundSpeed.m b/+kwave/+toolbox/@Medium/soundSpeed.m deleted file mode 100644 index 24062253..00000000 --- a/+kwave/+toolbox/@Medium/soundSpeed.m +++ /dev/null @@ -1,50 +0,0 @@ -%% soundSpeed -% *Class:* kwave.toolbox.Medium -% *Package:* kwave.toolbox -% -% Calls the soundSpeed across the spatial grid. -% -%% Syntax -% soundSpeed(obj) -% -%% Description -% recalls the sound speed spatial distribution according to the materialIDGrid, -% and the MaterialTable given as part of the medium. this is performed by -% generating the padded Density and removing the PML. -% -%% See Also -% * |soundSpeedPadded| - -% Copyright (C) 2025- The k-Wave Authors. -% -% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free -% software: you can redistribute it and/or modify it under the terms of the -% GNU Lesser General Public License as published by the Free Software -% Foundation, either version 3 of the License, or (at your option) any -% later version. -% -% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT -% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public -% License for more details. -% -% You should have received a copy of the GNU Lesser General Public License -% along with k-Wave-II. If not, see . - -function sS=soundSpeed(obj) -arguments - obj -end -if ~isscalar(obj.soundSpeedPadded) - V=obj.soundSpeedPadded; - switch obj.kgrid.dimensions - case 1 - sS=V(obj.kgrid.gridPadding(1)+1:end-obj.kgrid.gridPadding(1)); - case 2 - sS=V(obj.kgrid.gridPadding(1)+1:end-obj.kgrid.gridPadding(1), obj.kgrid.gridPadding(2)+1:end-obj.kgrid.gridPadding(2)); - case 3 - sS=V(obj.kgrid.gridPadding(1)+1:end-obj.kgrid.gridPadding(1), obj.kgrid.gridPadding(2)+1:end-obj.kgrid.gridPadding(2), obj.kgrid.gridPadding(3)+1:end-obj.kgrid.gridPadding(3)); - end -else - sS=obj.soundSpeedPadded; -end \ No newline at end of file diff --git a/+kwave/+toolbox/@Medium/soundSpeedPadded.m b/+kwave/+toolbox/@Medium/soundSpeedPadded.m deleted file mode 100644 index a13dfcf9..00000000 --- a/+kwave/+toolbox/@Medium/soundSpeedPadded.m +++ /dev/null @@ -1,44 +0,0 @@ -%% soundSpeedPadded -% *Class:* kwave.toolbox.Medium -% *Package:* kwave.toolbox -% -% Calls the soundSpeed value across the padded spatial grid. -% -%% Syntax -% soundSpeedPadded(obj) -% -%% Description -% recalls the sound Speed spatial distribution according to the materialIDGrid, -% using the generated materialIDGridPadded, and the given MaterialTable -% from the medium. -% -% This method has been generated to allow interactions with the acoustic -% solver class, and the acousticMedium grid field property soundSpeed (soundSpeedPadded) - -% Copyright (C) 2025- The k-Wave Authors. -% -% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free -% software: you can redistribute it and/or modify it under the terms of the -% GNU Lesser General Public License as published by the Free Software -% Foundation, either version 3 of the License, or (at your option) any -% later version. -% -% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT -% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public -% License for more details. -% -% You should have received a copy of the GNU Lesser General Public License -% along with k-Wave-II. If not, see . - - -function sS=soundSpeedPadded(obj) -arguments - obj -end -if ~isscalar(obj.materialIDGridPadded) - sS=reshape(obj.materialTable(obj.materialIDGridPadded,1),size(obj.materialIDGridPadded)); -else - sS=obj.materialTable(obj.materialIDGridPadded,1); -end -end diff --git a/+kwave/+toolbox/@Medium/specificHeat.m b/+kwave/+toolbox/@Medium/specificHeat.m deleted file mode 100644 index 1bf4c0be..00000000 --- a/+kwave/+toolbox/@Medium/specificHeat.m +++ /dev/null @@ -1,50 +0,0 @@ -%% specificHeat -% *Class:* kwave.toolbox.Medium -% *Package:* kwave.toolbox -% -% Calls the specific heat across the spatial grid. -% -%% Syntax -% specificHeat(obj) -% -%% Description -% recalls the specific heat spatial distribution according to the materialIDGrid, -% and the MaterialTable given as part of the medium. this is performed by -% generating the padded Density and removing the PML. -% -%% See Also -% * |specificHeatPadded| - -% Copyright (C) 2025- The k-Wave Authors. -% -% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free -% software: you can redistribute it and/or modify it under the terms of the -% GNU Lesser General Public License as published by the Free Software -% Foundation, either version 3 of the License, or (at your option) any -% later version. -% -% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT -% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public -% License for more details. -% -% You should have received a copy of the GNU Lesser General Public License -% along with k-Wave-II. If not, see . - -function sS=specificHeat(obj) -arguments - obj -end -if ~isscalar(obj.specificHeatPadded) - V=obj.specificHeatPadded; - switch obj.kgrid.dimensions - case 1 - sS=V(obj.kgrid.gridPadding(1)+1:end-obj.kgrid.gridPadding(1)); - case 2 - sS=V(obj.kgrid.gridPadding(1)+1:end-obj.kgrid.gridPadding(1), obj.kgrid.gridPadding(2)+1:end-obj.kgrid.gridPadding(2)); - case 3 - sS=V(obj.kgrid.gridPadding(1)+1:end-obj.kgrid.gridPadding(1), obj.kgrid.gridPadding(2)+1:end-obj.kgrid.gridPadding(2), obj.kgrid.gridPadding(3)+1:end-obj.kgrid.gridPadding(3)); - end -else - sS=obj.specificHeatPadded; -end \ No newline at end of file diff --git a/+kwave/+toolbox/@Medium/specificHeatPadded.m b/+kwave/+toolbox/@Medium/specificHeatPadded.m deleted file mode 100644 index 298d1be0..00000000 --- a/+kwave/+toolbox/@Medium/specificHeatPadded.m +++ /dev/null @@ -1,44 +0,0 @@ -%% specificHeatPadded -% *Class:* kwave.toolbox.Medium -% *Package:* kwave.toolbox -% -% Calls the specificHeat value across the padded spatial grid. -% -%% Syntax -% specificHeatPadded(obj) -% -%% Description -% recalls the specificHeat spatial distribution according to the materialIDGrid, -% using the generated materialIDGridPadded, and the given MaterialTable -% from the medium. -% -% This method has been generated to allow interactions with the thermalSolver -% class, and the thermalMedium gridfield property specificHeat (specificHeatPadded) - -% Copyright (C) 2025- The k-Wave Authors. -% -% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free -% software: you can redistribute it and/or modify it under the terms of the -% GNU Lesser General Public License as published by the Free Software -% Foundation, either version 3 of the License, or (at your option) any -% later version. -% -% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT -% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public -% License for more details. -% -% You should have received a copy of the GNU Lesser General Public License -% along with k-Wave-II. If not, see . - - -function sS=specificHeatPadded(obj) -arguments - obj -end -if ~isscalar(obj.materialIDGridPadded) - sS=reshape(obj.materialTable(obj.materialIDGridPadded,6),size(obj.materialIDGridPadded)); -else - sS=obj.materialTable(obj.materialIDGridPadded,6); -end -end \ No newline at end of file diff --git a/+kwave/+toolbox/@Medium/thermalConductivity.m b/+kwave/+toolbox/@Medium/thermalConductivity.m deleted file mode 100644 index a3a57623..00000000 --- a/+kwave/+toolbox/@Medium/thermalConductivity.m +++ /dev/null @@ -1,50 +0,0 @@ -%% thermalConductivity -% *Class:* kwave.toolbox.Medium -% *Package:* kwave.toolbox -% -% Calls the thermal conductivity across the spatial grid. -% -%% Syntax -% thermalConductivity(obj) -% -%% Description -% recalls the thermal conductivity spatial distribution according to the materialIDGrid, -% and the MaterialTable given as part of the medium. this is performed by -% generating the padded Density and removing the PML. -% -%% See Also -% * |thermalConductivityPadded| - -% Copyright (C) 2025- The k-Wave Authors. -% -% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free -% software: you can redistribute it and/or modify it under the terms of the -% GNU Lesser General Public License as published by the Free Software -% Foundation, either version 3 of the License, or (at your option) any -% later version. -% -% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT -% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public -% License for more details. -% -% You should have received a copy of the GNU Lesser General Public License -% along with k-Wave-II. If not, see . - -function sS=thermalConductivity(obj) -arguments - obj -end -if ~isscalar(obj.thermalConductivityPadded) - V=obj.thermalConductivityPadded; - switch obj.kgrid.dimensions - case 1 - sS=V(obj.kgrid.gridPadding(1)+1:end-obj.kgrid.gridPadding(1)); - case 2 - sS=V(obj.kgrid.gridPadding(1)+1:end-obj.kgrid.gridPadding(1), obj.kgrid.gridPadding(2)+1:end-obj.kgrid.gridPadding(2)); - case 3 - sS=V(obj.kgrid.gridPadding(1)+1:end-obj.kgrid.gridPadding(1), obj.kgrid.gridPadding(2)+1:end-obj.kgrid.gridPadding(2), obj.kgrid.gridPadding(3)+1:end-obj.kgrid.gridPadding(3)); - end -else - sS=obj.thermalConductivityPadded; -end \ No newline at end of file diff --git a/+kwave/+toolbox/@Medium/thermalConductivityPadded.m b/+kwave/+toolbox/@Medium/thermalConductivityPadded.m deleted file mode 100644 index 2bfa2c58..00000000 --- a/+kwave/+toolbox/@Medium/thermalConductivityPadded.m +++ /dev/null @@ -1,43 +0,0 @@ -%% ThermalConductivityPadded -% *Class:* kwave.toolbox.Medium -% *Package:* kwave.toolbox -% -% Calls the ThermalConductivity value across the padded spatial grid. -% -%% Syntax -% ThermalConductivityPadded(obj) -% -%% Description -% recalls the ThermalConductivity spatial distribution according to the materialIDGrid, -% using the generated materialIDGridPadded, and the given MaterialTable -% from the medium. -% -% This method has been generated to allow interactions with the thermalSolver -% class, and the thermalMedium gridfield property ThermalConductivity (ThermalConductivityPadded) - -% Copyright (C) 2025- The k-Wave Authors. -% -% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free -% software: you can redistribute it and/or modify it under the terms of the -% GNU Lesser General Public License as published by the Free Software -% Foundation, either version 3 of the License, or (at your option) any -% later version. -% -% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT -% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or -% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public -% License for more details. -% -% You should have received a copy of the GNU Lesser General Public License -% along with k-Wave-II. If not, see . - -function sS=thermalConductivityPadded(obj) -arguments - obj -end -if ~isscalar(obj.materialIDGridPadded) - sS=reshape(obj.materialTable(obj.materialIDGridPadded,7),size(obj.materialIDGridPadded)); -else - sS=obj.materialTable(obj.materialIDGridPadded,7); -end -end \ No newline at end of file diff --git a/+kwave/+toolbox/@Sensor/Sensor.m b/+kwave/+toolbox/@Sensor/Sensor.m new file mode 100644 index 00000000..2c3cba73 --- /dev/null +++ b/+kwave/+toolbox/@Sensor/Sensor.m @@ -0,0 +1,84 @@ +%% Sensor +% *Package:* kwave.toolbox +% *Superclasses:* kwave.toolbox.GridInput +% +% Superclass of all kwave.toolbox sensor classes. +% +%% Description +% Abstract class used to define sensors. All sensor classes should be +% derived from this class. +% +%% Properties +% * |mask| - Binary mask the size of the grid with 1s indicated +% sensor positions. +% +%% See Also +% * |kwave.toolbox.AcousticSensor| +% * |kwave.toolbox.Grid| +% * |kwave.toolbox.GridInput| + +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + +classdef Sensor < kwave.toolbox.GridInput + + properties(Constant, Hidden=true) + + requiredProperties = {'mask'}; + + gridFields = kwave.toolbox.GridField.createGridFieldsMap([ + kwave.toolbox.GridField('mask', Classes={'logical','numeric'},... + Attributes={'binary'})]); + end + + properties + timeSteps(1,1) {mustBeInteger, mustBeFinite, mustBePositive} = 1; + end + + properties(Hidden) + sensorIndex(1,1) single {mustBeInteger, mustBeFinite, mustBeNonnegative} = 0; + totalSensorPoints =0; + end + + methods + function totalSensorPoints=get.totalSensorPoints(obj) + obj.totalSensorPoints= sum(obj.kgrid.returnWithoutGridPadding(obj.maskPadded),'all'); + totalSensorPoints=obj.totalSensorPoints; + end + % function obj=multiDimMask(obj) + % obj.multiDimMask=zeros(obj.kgrid.gridSize,obj.kgrid.dimensions); + % for dim=1:obj.kgrid.dimensions + % obj.multiDimMask(:,:,:,dim)=obj.kgrid.returnWithoutGridPadding(obj.maskPadded); + % end + % end + function SensorOutput = ProcessSensorData(obj,Variable,dim,string) + mask = obj.kgrid.returnWithoutGridPadding(obj.maskPadded); + if ~strcmp(string,'sum') + SensorOutput = single(zeros(obj.totalSensorPoints,dim)); + for dimension = 1:dim + Var = Variable(:,:,:,dimension); + SensorOutput(:,dimension) = Var(mask==1); + end + else + SensorOutput = single(zeros(obj.totalSensorPoints,1)); + for dimension = 1:dim + Var = Variable(:,:,:,dimension); + SensorOutput = SensorOutput + Var(mask==1); + end + end + end + end +end diff --git a/+kwave/+toolbox/@Settings/Settings.m b/+kwave/+toolbox/@Settings/Settings.m index 877b3b4f..f9a87ef4 100644 --- a/+kwave/+toolbox/@Settings/Settings.m +++ b/+kwave/+toolbox/@Settings/Settings.m @@ -14,8 +14,8 @@ % %% Properties % * |colorMap| - (numeric) Color map used for 2D plots (see -% ). Default is the k-Wave color map. -% * |plotFrequency| - (integer) The number of iterations which must pass +% ). Default is the k-Wave color map. +% * |plotFrequency| - (integer) The number of iterations which must pass % before the simulation plot is updated. Default = 10. % * |plotScale| - (numeric) [min, max] values used to control the scaling % for imagesc (visualisation). Default = [-1, 1]. @@ -24,22 +24,23 @@ % * |simulationDataType| - ('single', 'double') Data-type used for % simulation calculations. Default = 'single'. -% Copyright (C) 2022- University College London. +% Copyright (C) 2024- The k-Wave Authors. % % This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free % software: you can redistribute it and/or modify it under the terms of the % GNU Lesser General Public License as published by the Free Software % Foundation, either version 3 of the License, or (at your option) any % later version. -% +% % k-Wave-II is distributed in the hope that it will be useful, but WITHOUT % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or % FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public % License for more details. -% +% % You should have received a copy of the GNU Lesser General Public License % along with k-Wave-II. If not, see . + classdef Settings < handle properties colorMap(:,3) double = kwave.legacy.getColorMap diff --git a/+kwave/+toolbox/@Solver/Solver.m b/+kwave/+toolbox/@Solver/Solver.m index facce62d..3e8f685a 100644 --- a/+kwave/+toolbox/@Solver/Solver.m +++ b/+kwave/+toolbox/@Solver/Solver.m @@ -17,32 +17,33 @@ % The |Solver| class is derived from |kwave.toolbox.FourierCollocation|. % This superclass implements common methods to calculate differential % operators using the Fourier collocation spectral method. See class -% documentation for futher details. +% documentation for further details. % % The constructor calls the |checkRequiredProperties| method for the input -% medium, source, and sensor objects. +% medium, source, and sensor objects. % % Classes derived from |Solver| should define padded variants of % any PDE variables that can be accessed by the user, and implement set and % get methods that add and remove the grid padding. % %% Input Arguments -% * |kgrid| - (kwave.toolbox.Grid) Object which defines the simulation grid -% size. -% * |medium| - (kwave.toolbox.GridInput) Object which defines the medium -% properties. -% * |source| - (kwave.toolbox.GridInput) Object which defines the source -% properties. -% * |sensor| - ...Not yet implemented... +% * |kgrid| - (kwave.toolbox.Grid) Object which defines the simulation +% grid size. +% * |medium| - (kwave.toolbox.GridInput) Object which defines the medium +% properties. +% * |source| - (kwave.toolbox.GridInput) Object which defines the source +% properties. +% * |sensor| - (kwave.toolbox.sensor) Object which defines the sensor +% properties. % * |settings| - (kwave.toolbox.Settings) Object which defines the -% simulation settings. +% simulation settings. % %% Properties % Input objects: % -% * |medium| - (kwave.toolbox.GridInput) Handle for medium object. -% * |source| - (kwave.toolbox.GridInput) Handle for source object. -% * |sensor| - ...Not yet implemented... +% * |medium| - (kwave.toolbox.GridInput) Handle for medium object. +% * |source| - (kwave.toolbox.GridInput) Handle for source object. +% * |sensor| - (kwave.toolbox.sensor) Handle for sensor object. % * |settings| - (kwave.toolbox.Settings) Handle for settings object. % %% Template Methods @@ -52,6 +53,7 @@ % * |kwave.toolbox.FourierCollocation| % * |kwave.toolbox.Grid| % * |kwave.toolbox.GridInput| +% * |kwave.toolbox.Sensor| % * |kwave.toolbox.Settings| % Copyright (C) 2024- The k-Wave Authors. @@ -61,12 +63,12 @@ % GNU Lesser General Public License as published by the Free Software % Foundation, either version 3 of the License, or (at your option) any % later version. -% +% % k-Wave-II is distributed in the hope that it will be useful, but WITHOUT % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or % FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public % License for more details. -% +% % You should have received a copy of the GNU Lesser General Public License % along with k-Wave-II. If not, see . @@ -101,6 +103,11 @@ if (source.kgrid ~= kgrid) kwave.toolbox.Logger.error('Solver:gridMismatch', 'The source input references a different Grid object to the kgrid input.'); end + if isprop(sensor,'kgrid') + if (sensor.kgrid ~= kgrid) + kwave.toolbox.Logger.error('Solver:gridMismatch', 'The source input references a different Grid object to the kgrid input.'); + end + end % Check the required input properties have been defined. medium.checkRequiredProperties; diff --git a/+kwave/+toolbox/@SplitFieldPML/SplitFieldPML.m b/+kwave/+toolbox/@SplitFieldPML/SplitFieldPML.m index 73989dcc..2a1ca52c 100644 --- a/+kwave/+toolbox/@SplitFieldPML/SplitFieldPML.m +++ b/+kwave/+toolbox/@SplitFieldPML/SplitFieldPML.m @@ -54,12 +54,12 @@ % GNU Lesser General Public License as published by the Free Software % Foundation, either version 3 of the License, or (at your option) any % later version. -% +% % k-Wave-II is distributed in the hope that it will be useful, but WITHOUT % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or % FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public % License for more details. -% +% % You should have received a copy of the GNU Lesser General Public License % along with k-Wave-II. If not, see . @@ -175,7 +175,7 @@ function pml = get.pmlZStaggered(obj) pml = obj.privatePmlZStaggered; - end + end end diff --git a/+kwave/+toolbox/@SplitFieldPML/applyPML.m b/+kwave/+toolbox/@SplitFieldPML/applyPML.m index 83e3a32f..c0da1a74 100644 --- a/+kwave/+toolbox/@SplitFieldPML/applyPML.m +++ b/+kwave/+toolbox/@SplitFieldPML/applyPML.m @@ -1,4 +1,4 @@ -%% applyPML +%% Apply PML % *Package:* kwave.toolbox % *Class:* kwave.toolbox.SplitFieldPML % @@ -37,12 +37,12 @@ % GNU Lesser General Public License as published by the Free Software % Foundation, either version 3 of the License, or (at your option) any % later version. -% +% % k-Wave-II is distributed in the hope that it will be useful, but WITHOUT % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or % FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public % License for more details. -% +% % You should have received a copy of the GNU Lesser General Public License % along with k-Wave-II. If not, see . diff --git a/+kwave/+toolbox/@SplitFieldPML/getQuarticPMLProfile.m b/+kwave/+toolbox/@SplitFieldPML/getQuarticPMLProfile.m index 799e9861..addfbead 100644 --- a/+kwave/+toolbox/@SplitFieldPML/getQuarticPMLProfile.m +++ b/+kwave/+toolbox/@SplitFieldPML/getQuarticPMLProfile.m @@ -1,4 +1,4 @@ -%% getQuarticPMLprofile +%% Get Quartic PML profile % *Package:* kwave.toolbox % *Class:* kwave.toolbox.SplitFieldPML % @@ -50,12 +50,12 @@ % GNU Lesser General Public License as published by the Free Software % Foundation, either version 3 of the License, or (at your option) any % later version. -% +% % k-Wave-II is distributed in the hope that it will be useful, but WITHOUT % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or % FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public % License for more details. -% +% % You should have received a copy of the GNU Lesser General Public License % along with k-Wave-II. If not, see . @@ -78,8 +78,8 @@ % Create absorption profile within the PML. if options.Staggered - leftPML = options.PMLAlpha * (soundSpeed / gridSpacing) * ( ((x + 0.5) - options.PMLSize - 1) ./ (0 - options.PMLSize) ).^4; - rightPML = options.PMLAlpha * (soundSpeed / gridSpacing) * ( (x + 0.5) ./ options.PMLSize ).^4; + leftPML = options.PMLAlpha * (soundSpeed / gridSpacing) * ( ((x + 0.5) - options.PMLSize - 1) ./ (0 - options.PMLSize) ).^4; + rightPML = options.PMLAlpha * (soundSpeed / gridSpacing) * ( (x + 0.5) ./ options.PMLSize ).^4; else leftPML = options.PMLAlpha * (soundSpeed / gridSpacing) * ( (x - options.PMLSize - 1) ./ (0 - options.PMLSize) ).^4; rightPML = options.PMLAlpha * (soundSpeed / gridSpacing) * ( x ./ options.PMLSize ).^4; diff --git a/+kwave/+toolbox/@SplitFieldPML/setupQuarticPML.m b/+kwave/+toolbox/@SplitFieldPML/setupQuarticPML.m index 3d641b75..3e18fbdb 100644 --- a/+kwave/+toolbox/@SplitFieldPML/setupQuarticPML.m +++ b/+kwave/+toolbox/@SplitFieldPML/setupQuarticPML.m @@ -1,4 +1,4 @@ -%% setupQuarticPML +%% Setup Quartic PML % *Package:* kwave.toolbox % *Class:* kwave.toolbox.SplitFieldPML % @@ -28,12 +28,12 @@ % GNU Lesser General Public License as published by the Free Software % Foundation, either version 3 of the License, or (at your option) any % later version. -% +% % k-Wave-II is distributed in the hope that it will be useful, but WITHOUT % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or % FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public % License for more details. -% +% % You should have received a copy of the GNU Lesser General Public License % along with k-Wave-II. If not, see . @@ -64,7 +64,7 @@ function setupQuarticPML(obj, timeStep, soundSpeed) obj.kgrid.Ny + 2 * pmlSize(2), obj.kgrid.dy, ... timeStep, soundSpeed, ... Dimension=2, PMLSize=pmlSize(2), PMLAlpha=obj.pmlAlpha(2)); - + obj.pmlYStaggered = obj.getQuarticPMLProfile(... obj.kgrid.Ny + 2 * pmlSize(2), obj.kgrid.dy, ... timeStep, soundSpeed, ... @@ -77,7 +77,7 @@ function setupQuarticPML(obj, timeStep, soundSpeed) obj.kgrid.Nz + 2 * pmlSize(3), obj.kgrid.dz, ... timeStep, soundSpeed, ... Dimension=3, PMLSize=pmlSize(3), PMLAlpha=obj.pmlAlpha(3)); - + obj.pmlZStaggered = obj.getQuarticPMLProfile(... obj.kgrid.Nz + 2 * pmlSize(3), obj.kgrid.dz, ... timeStep, soundSpeed, ... diff --git a/+kwave/+toolbox/@ThermalMedium/ThermalMedium.m b/+kwave/+toolbox/@ThermalMedium/ThermalMedium.m index 87881151..5f99472d 100644 --- a/+kwave/+toolbox/@ThermalMedium/ThermalMedium.m +++ b/+kwave/+toolbox/@ThermalMedium/ThermalMedium.m @@ -37,6 +37,22 @@ %% See Also % * |GridInput| +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef ThermalMedium < kwave.toolbox.GridInput properties diff --git a/+kwave/+toolbox/@ThermalSolver/ThermalSolver.m b/+kwave/+toolbox/@ThermalSolver/ThermalSolver.m index 32f1e6a3..aa9e1c5d 100644 --- a/+kwave/+toolbox/@ThermalSolver/ThermalSolver.m +++ b/+kwave/+toolbox/@ThermalSolver/ThermalSolver.m @@ -37,24 +37,24 @@ % % clearvars; % import kwave.toolbox.* -% +% % % Grid. % kgrid = Grid(128, 1e-3); -% +% % % Medium. % medium = ThermalMedium(kgrid); % medium.thermalConductivity = 0.52; % medium.specificHeat = 3540; % medium.density = 1000; -% +% % % Source. % source = ThermalSource(kgrid); % source.initialTemperature = exp( -kgrid.xVec.^2 ./ (10 * kgrid.dx).^2 ); -% +% % % Solve. % solver = ThermalSolver(kgrid, medium, source, []); % solver.run(Nt=500, dt=0.5); -% +% % % Plot. % figure; % plot(1e3 * kgrid.xVec, source.initialTemperature); @@ -79,23 +79,29 @@ % %% Properties % * |temperature| - (numeric) Temperature field [degC]. +% +%% Methods +% * |autoComputeTimeStep| +% * |executeTimeStep| +% * |setInitialConditions| -% Copyright (C) 2022- University College London. +% Copyright (C) 2024- The k-Wave Authors. % % This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free % software: you can redistribute it and/or modify it under the terms of the % GNU Lesser General Public License as published by the Free Software % Foundation, either version 3 of the License, or (at your option) any % later version. -% +% % k-Wave-II is distributed in the hope that it will be useful, but WITHOUT % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or % FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public % License for more details. -% +% % You should have received a copy of the GNU Lesser General Public License % along with k-Wave-II. If not, see . + classdef ThermalSolver < kwave.toolbox.TimeDomainSolver % PDE variables. @@ -130,7 +136,7 @@ end end - + % Get methods for PDE variables on non-padded grid. methods function temperature = get.temperature(obj) diff --git a/+kwave/+toolbox/@ThermalSolver/autoComputeTimeStep.m b/+kwave/+toolbox/@ThermalSolver/autoComputeTimeStep.m index ecbf5bf9..52771580 100644 --- a/+kwave/+toolbox/@ThermalSolver/autoComputeTimeStep.m +++ b/+kwave/+toolbox/@ThermalSolver/autoComputeTimeStep.m @@ -1,4 +1,4 @@ -%% autoComputeTimeStep +%% autoCompute Time Step % *Class:* kwave.toolbox.ThermalSolver % *Package:* kwave.toolbox % @@ -26,12 +26,12 @@ % GNU Lesser General Public License as published by the Free Software % Foundation, either version 3 of the License, or (at your option) any % later version. -% +% % k-Wave-II is distributed in the hope that it will be useful, but WITHOUT % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or % FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public % License for more details. -% +% % You should have received a copy of the GNU Lesser General Public License % along with k-Wave-II. If not, see . diff --git a/+kwave/+toolbox/@ThermalSolver/executeTimeStep.m b/+kwave/+toolbox/@ThermalSolver/executeTimeStep.m index 15ec20fc..eb03e86b 100644 --- a/+kwave/+toolbox/@ThermalSolver/executeTimeStep.m +++ b/+kwave/+toolbox/@ThermalSolver/executeTimeStep.m @@ -1,4 +1,4 @@ -%% executeTimeStep +%% Execute Time Step % *Class:* kwave.toolbox.ThermalSolver % *Package:* kwave.toolbox % @@ -13,24 +13,25 @@ % %% Input Arguments % * |Nt| - (integer) Number of time steps. -% * |dt| - (numeric) Size of each time step. +% * |dt| - (numeric) Size of each time step. -% Copyright (C) 2022- University College London. +% Copyright (C) 2024- The k-Wave Authors. % % This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free % software: you can redistribute it and/or modify it under the terms of the % GNU Lesser General Public License as published by the Free Software % Foundation, either version 3 of the License, or (at your option) any % later version. -% +% % k-Wave-II is distributed in the hope that it will be useful, but WITHOUT % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or % FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public % License for more details. -% +% % You should have received a copy of the GNU Lesser General Public License % along with k-Wave-II. If not, see . + function executeTimeStep(obj, Nt, dt) arguments diff --git a/+kwave/+toolbox/@ThermalSolver/setInitialConditions.m b/+kwave/+toolbox/@ThermalSolver/setInitialConditions.m index 6dee7ade..2eac1e23 100644 --- a/+kwave/+toolbox/@ThermalSolver/setInitialConditions.m +++ b/+kwave/+toolbox/@ThermalSolver/setInitialConditions.m @@ -1,4 +1,4 @@ -%% setInitialConditions +%% Set Initial Conditions % *Class:* kwave.toolbox.ThermalSolver % *Package:* kwave.toolbox % @@ -12,22 +12,23 @@ % |obj.source.initialTemperaturePadded| if defined, or zeros if not. Also % assigns the reference diffusion coefficient if not provided. -% Copyright (C) 2022- University College London. +% Copyright (C) 2024- The k-Wave Authors. % % This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free % software: you can redistribute it and/or modify it under the terms of the % GNU Lesser General Public License as published by the Free Software % Foundation, either version 3 of the License, or (at your option) any % later version. -% +% % k-Wave-II is distributed in the hope that it will be useful, but WITHOUT % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or % FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public % License for more details. -% +% % You should have received a copy of the GNU Lesser General Public License % along with k-Wave-II. If not, see . + function setInitialConditions(obj) % Assign the reference diffusion coefficient if not provided. Using the diff --git a/+kwave/+toolbox/@ThermalSource/ThermalSource.m b/+kwave/+toolbox/@ThermalSource/ThermalSource.m index bf308dc9..e313cbda 100644 --- a/+kwave/+toolbox/@ThermalSource/ThermalSource.m +++ b/+kwave/+toolbox/@ThermalSource/ThermalSource.m @@ -25,6 +25,22 @@ %% See Also % * |GridInput| +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + classdef ThermalSource < kwave.toolbox.GridInput properties(Constant, Hidden=true) diff --git a/+kwave/+toolbox/@TimeDomainSolver/TimeDomainSolver.m b/+kwave/+toolbox/@TimeDomainSolver/TimeDomainSolver.m index afa1bbe0..9693eed4 100644 --- a/+kwave/+toolbox/@TimeDomainSolver/TimeDomainSolver.m +++ b/+kwave/+toolbox/@TimeDomainSolver/TimeDomainSolver.m @@ -25,20 +25,22 @@ % get methods that add and remove the grid padding. % %% Input Arguments -% * |kgrid| - (kwave.toolbox.Grid) Object which defines the simulation grid -% size. -% * |medium| - (kwave.toolbox.GridInput) Object which defines the medium -% properties. -% * |source| - (kwave.toolbox.GridInput) Object which defines the source -% properties. -% * |sensor| - ...Not yet implemented... +% * |kgrid| - (kwave.toolbox.Grid) Object which defines the simulation +% grid size. +% * |medium| - (kwave.toolbox.GridInput) Object which defines the medium +% properties. +% * |source| - (kwave.toolbox.GridInput) Object which defines the source +% properties. +% * |sensor| - (kwave.toolbox.sensor) Object which defines the sensor +% properties. % * |settings| - (kwave.toolbox.Settings) Object which defines the -% simulation settings. +% simulation settings. % %% Properties -% * |prevTimeStep| - (single) Size of the time step used in the last call -% to |run|. Set to an empty array if |run| hasn't been called. -% * |timeArray| - (single) Time points at which update steps were taken. +% * |prevTimeStep| - (single) Size of the time step used in the last call +% to |run|. Set to an empty array if |run| hasn't been +% called. +% * |timeArray| - (single) Time points at which update steps were taken. % * |timeStepsTaken| - (integer) Number of time steps taken. % %% Methods @@ -50,22 +52,23 @@ % * |kwave.toolbox.GridInput| % * |kwave.toolbox.Settings| -% Copyright (C) 2022- University College London. +% Copyright (C) 2024- The k-Wave Authors. % % This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free % software: you can redistribute it and/or modify it under the terms of the % GNU Lesser General Public License as published by the Free Software % Foundation, either version 3 of the License, or (at your option) any % later version. -% +% % k-Wave-II is distributed in the hope that it will be useful, but WITHOUT % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or % FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public % License for more details. -% +% % You should have received a copy of the GNU Lesser General Public License % along with k-Wave-II. If not, see . + classdef(Abstract) TimeDomainSolver < kwave.toolbox.Solver properties(SetAccess=protected) diff --git a/+kwave/+toolbox/@TimeDomainSolver/run.m b/+kwave/+toolbox/@TimeDomainSolver/run.m index 30a2f0d3..2242cf70 100644 --- a/+kwave/+toolbox/@TimeDomainSolver/run.m +++ b/+kwave/+toolbox/@TimeDomainSolver/run.m @@ -1,4 +1,4 @@ -%% run +%% The Run Method % *Class:* kwave.toolbox.TimeDomainSolver % *Package:* kwave.toolbox % @@ -38,59 +38,64 @@ % * |CFL| - (numeric) Courant-Friedrichs-Lewy (CFL) number. % * |EndTime| - (numeric) Simulation time [s]. -% Copyright (C) 2022- University College London. +% Copyright (C) 2024- The k-Wave Authors. % % This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free % software: you can redistribute it and/or modify it under the terms of the % GNU Lesser General Public License as published by the Free Software % Foundation, either version 3 of the License, or (at your option) any % later version. -% +% % k-Wave-II is distributed in the hope that it will be useful, but WITHOUT % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or % FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public % License for more details. -% +% % You should have received a copy of the GNU Lesser General Public License % along with k-Wave-II. If not, see . + function run(obj, options) -arguments - obj - options.Nt {mustBeScalarOrEmpty, mustBeInteger, mustBeNonnegative, mustBeFinite} = [] - options.dt {mustBeScalarOrEmpty, mustBeNumeric, mustBePositive, mustBeFinite} = [] - options.CFL {mustBeScalarOrEmpty, mustBeNumeric, mustBePositive, mustBeFinite} = [] - options.EndTime {mustBeScalarOrEmpty, mustBeNumeric, mustBePositive, mustBeFinite} = [] -end + arguments + obj + options.Nt {mustBeScalarOrEmpty, mustBeInteger, mustBeNonnegative, mustBeFinite} = [] + options.dt {mustBeScalarOrEmpty, mustBeNumeric, mustBePositive, mustBeFinite} = [] + options.CFL {mustBeScalarOrEmpty, mustBeNumeric, mustBePositive, mustBeFinite} = [] + options.EndTime {mustBeScalarOrEmpty, mustBeNumeric, mustBePositive, mustBeFinite} = [] + end -kwave.utilities.mustBeAllOrNoneEmpty(options.Nt, options.dt) -if isempty(options.Nt) - [options.Nt, options.dt] = obj.autoComputeTimeStep(options.CFL, options.EndTime); -end + kwave.utilities.mustBeAllOrNoneEmpty(options.Nt, options.dt) + if isempty(options.Nt) + [options.Nt, options.dt] = obj.autoComputeTimeStep(options.CFL, options.EndTime); + end -startTime = datetime('now'); + startTime = datetime('now'); -kwave.toolbox.Logger.info(['Calling ' class(obj) '.run...']); -obj.kgrid.displayGridSize(); -kwave.toolbox.Logger.info([' dt: ' kwave.utilities.scaleSI(options.dt) 's, end time: ' kwave.utilities.scaleSI(options.dt * options.Nt) 's, time steps: ' num2str(options.Nt)]); + kwave.toolbox.Logger.info(['Calling ' class(obj) '.run...']); + obj.kgrid.displayGridSize(); + kwave.toolbox.Logger.info([' dt: ' kwave.utilities.scaleSI(options.dt) 's, end time: ' kwave.utilities.scaleSI(options.dt * single(options.Nt)) 's, time steps: ' num2str(options.Nt)]); -if (obj.timeStepsTaken==0) - % Set initial conditions. - obj.setInitialConditions; -end + if (obj.timeStepsTaken==0) + % Set initial conditions. + obj.setInitialConditions; + if ~isempty(obj.sensor) + obj.sensor.initialiseSensorData(options.Nt); + end + end -obj.executeTimeStep(options.Nt, options.dt); + % Take time steps + obj.executeTimeStep(options.Nt, options.dt); -elapsedTime = between(startTime, datetime('now')); -kwave.toolbox.Logger.info([' run completed in ' kwave.utilities.formatDuration(elapsedTime)]); + elapsedTime = between(startTime, datetime('now')); + kwave.toolbox.Logger.info([' run completed in ' kwave.utilities.formatDuration(elapsedTime)]); -% Update time variables. -obj.prevTimeStep = options.dt; -obj.timeStepsTaken = obj.timeStepsTaken + options.Nt; -if isempty(obj.timeArray) - obj.timeArray = (0:(options.Nt)) * options.dt; -else - obj.timeArray = [obj.timeArray, obj.timeArray(end) + (1:options.Nt) * options.dt]; -end + % Update time variables. + obj.prevTimeStep = options.dt; + obj.timeStepsTaken = obj.timeStepsTaken + options.Nt; + if isempty(obj.timeArray) + obj.timeArray = (0:(single(options.Nt))) * options.dt; + else + obj.timeArray = [obj.timeArray, obj.timeArray(end) + (1:single(options.Nt)) * options.dt]; + end end diff --git a/+kwave/+toolbox/BuildMaterialTable.m b/+kwave/+toolbox/BuildMaterialTable.m deleted file mode 100644 index 00c57eaf..00000000 --- a/+kwave/+toolbox/BuildMaterialTable.m +++ /dev/null @@ -1,54 +0,0 @@ -%% BuildMaterialTable -% *Package:* kwave.toolbox -% -% Builds the defaulf Material Table for the medium class. -% -%% Syntax -% MatTab = BuildMaterialTable() -% -%% Description -% * |BuildMaterialTable| constructs a default 6x7 matrix of parameter -% values according to given materials. -% Parameter values are gven for a particular material aas a 1x7 vector -% containing, in order: -% soundSpeed (Acoustic) -% density (Acoustic and Thermal) -% absorptionCoeff (Acoustic) -% absorptionPower (Acoustic) -% BonA (Acoustic) -% specificHeat (Thermal) -% thermalConductivity (Thermal) -% New materials may be added in this way appended onto the Build Material -% Table, or other construction, however this parameter order remains fixed. -% Current matierials given are for testing only; -% With a general Acoustic and Themermal test case (1,:) -% Acoustic test case 2 (2,:) -% Acoustic Test Case 3 (3,:) -% Acoustic Test Case 4 (4,:) -% Random values at time of table generation (5,:) -% All Zeros (6,:) -% parameter values not required for the problem type may be set to zero, -% however in general this is not the case. -% -%% Input Arguments -% -%% Output Arguments -% * |MtTab| - (numeric) 6x7 matrix of parameter values. -% -%% See Also -% * |Medium| - -function MatTab = BuildMaterialTable() - -MatTab=zeros(6,7); -MatTab(1,:)=[1500 ,1000 ,10 ,1.9,0,3540,0.52]; % Test 1 -MatTab(2,:)=[1500/0.9,1000 ,10 ,1.9,0,0,0]; % Test Acoustic 2 -MatTab(3,:)=[1500 ,1000/1.1,10 ,1.9,0,0,0]; % Test Acoustic 3 -MatTab(4,:)=[1500 ,1000 ,10/1.1,1.9,0,0,0]; % Test Acoustic 4 -MatTab(5,:)=rand(1,7); % Random Values -MatTab(6,:)=0; % All Zero - -MatTab=single(MatTab(:,:)); - -end - diff --git a/+kwave/+toolbox/Toolbox_Functions.m b/+kwave/+toolbox/Toolbox_Functions.m new file mode 100644 index 00000000..09e91b8f --- /dev/null +++ b/+kwave/+toolbox/Toolbox_Functions.m @@ -0,0 +1,38 @@ +%% Toolbox Functions +% * |BuildMaterialTable| +% * |expandMatrix| +% * |AcousticMedium| +% * |AcousticSolver| +% * |AcousticSource| +% * |ComplexSource| +% * |FourierCollocation| +% * |Grid| +% * |GridField| +% * |GridFieldType| +% * |GridInput| +% * |LogLevels| +% * |Logger| +% * |Medium| +% * |Settings| +% * |Solver| +% * |SplitFieldPML| +% * |ThermalMedium| +% * |ThermalSolver| +% * |ThermalSource| +% * |TimeDomainSolver| + +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . diff --git a/+kwave/+toolbox/expandMatrix.m b/+kwave/+toolbox/expandMatrix.m index 19c416f6..e59b9e0b 100644 --- a/+kwave/+toolbox/expandMatrix.m +++ b/+kwave/+toolbox/expandMatrix.m @@ -1,4 +1,4 @@ -%% expandMatrix +%% Expand Matrix % *Package:* kwave.toolbox % % Enlarge a matrix by extending the edge values. @@ -18,17 +18,13 @@ %% Examples % % matrix = magic(3) -% % matrix = -% % 8 1 6 % 3 5 7 % 4 9 2 % % kwave.toolbox.expandMatrix(matrix, 1) -% % ans = -% % 8 8 1 6 6 % 8 8 1 6 6 % 3 3 5 7 7 @@ -36,9 +32,7 @@ % 4 4 9 2 2 % % kwave.toolbox.expandMatrix(matrix, [2 0 1 0], 0) -% % ans = -% % 0 0 0 0 % 0 0 0 0 % 0 8 1 6 @@ -46,6 +40,7 @@ % 0 4 9 2 % %% Input Arguments +% % * |matrix| - (numeric) A 1D, 2D, or 3D matrix to expand. % * |expansionSize| - (integer) The expansion size. There are several ways % to specify the size. If a scalar value is given, this expansion is @@ -59,8 +54,25 @@ % expansion. % %% Output Arguments +% % * |matrixExpanded| - (numeric) Expanded matrix. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + function matrixExpanded = expandMatrix(matrix, expansionSize, expansionValue) arguments @@ -86,18 +98,18 @@ end if isvector(matrix) % 1D vector - + % Extract expansion sizes. if length(expansionSize) == 2 x1Expansion = expansionSize(1); x2Expansion = expansionSize(2); - elseif isscalar(expansionSize) + elseif isscalar(expansionSize) x1Expansion = expansionSize(1); x2Expansion = expansionSize(1); else kwave.toolbox.Logger.error('expandMatrix:incorrectInputSize', 'For 1D matrices, expansionSize must be given as [exp] or [xStartExp, xEndExp].'); end - + % Create expanded matrix. xSize = length(matrix) + x1Expansion + x2Expansion; if isrow(matrix) @@ -115,7 +127,7 @@ % if specified. if extendEdges matrixExpanded(1:(x1 - 1)) = matrix(1); - matrixExpanded((x2 + 1):end) = matrix(end); + matrixExpanded((x2 + 1):end) = matrix(end); else matrixExpanded(:) = expansionValue; end @@ -124,7 +136,7 @@ matrixExpanded(x1:x2) = matrix; elseif ismatrix(matrix) % 2D matrix - + % Extract expansion sizes. if length(expansionSize) == 4 x1Expansion = expansionSize(1); @@ -135,7 +147,7 @@ x1Expansion = expansionSize(1); x2Expansion = expansionSize(1); y1Expansion = expansionSize(2); - y2Expansion = expansionSize(2); + y2Expansion = expansionSize(2); elseif isscalar(expansionSize) x1Expansion = expansionSize; x2Expansion = expansionSize; @@ -166,7 +178,7 @@ matrixExpanded(x2+1:end, y1:y2) = repmat(matrix(end, :), x2Expansion, 1); matrixExpanded(x1:x2, 1:y1-1) = repmat(matrix(:, 1), 1, y1Expansion); matrixExpanded(x1:x2, y2+1:end) = repmat(matrix(:, end), 1, y2Expansion); - + % Extend corner values. matrixExpanded(1:x1-1, 1:y1-1) = matrix(1, 1) * ones(x1Expansion, y1Expansion, dataType); matrixExpanded(1:x1-1, y2+1:end) = matrix(1, end) * ones(x1Expansion, y2Expansion, dataType); @@ -187,21 +199,21 @@ x1Expansion = expansionSize(1); x2Expansion = expansionSize(2); y1Expansion = expansionSize(3); - y2Expansion = expansionSize(4); + y2Expansion = expansionSize(4); z1Expansion = expansionSize(5); z2Expansion = expansionSize(6); elseif length(expansionSize) == 3 x1Expansion = expansionSize(1); x2Expansion = expansionSize(1); y1Expansion = expansionSize(2); - y2Expansion = expansionSize(2); + y2Expansion = expansionSize(2); z1Expansion = expansionSize(3); z2Expansion = expansionSize(3); elseif isscalar(expansionSize) x1Expansion = expansionSize; x2Expansion = expansionSize; y1Expansion = expansionSize; - y2Expansion = expansionSize; + y2Expansion = expansionSize; z1Expansion = expansionSize; z2Expansion = expansionSize; else @@ -219,7 +231,7 @@ x1 = 1 + x1Expansion; x2 = xSize - x2Expansion; y1 = 1 + y1Expansion; - y2 = ySize - y2Expansion; + y2 = ySize - y2Expansion; z1 = 1 + z1Expansion; z2 = zSize - z2Expansion; @@ -233,8 +245,8 @@ matrixExpanded(x1:x2, 1:y1-1, z1:z2) = repmat(matrix(:, 1, :), [1, y1Expansion, 1]); matrixExpanded(x1:x2, y2+1:end, z1:z2) = repmat(matrix(:, end, :), [1, y2Expansion, 1]); matrixExpanded(x1:x2, y1:y2, 1:z1-1) = repmat(matrix(:, :, 1), [1, 1, z1Expansion]); - matrixExpanded(x1:x2, y1:y2, z2+1:end) = repmat(matrix(:, :, end), [1, 1, z2Expansion]); - + matrixExpanded(x1:x2, y1:y2, z2+1:end) = repmat(matrix(:, :, end), [1, 1, z2Expansion]); + % Extend edge values. matrixExpanded(1:x1-1, 1:y1-1, z1:z2) = repmat(matrix(1, 1, :), [x1Expansion, y1Expansion, 1]); matrixExpanded(1:x1-1, y2+1:end, z1:z2) = repmat(matrix(1, end, :), [x1Expansion, y2Expansion, 1]); @@ -248,7 +260,7 @@ matrixExpanded(x1:x2, y2+1:end, 1:z1-1) = repmat(matrix(:, end, 1), [1, y2Expansion, z1Expansion]); matrixExpanded(x1:x2, 1:y1-1, z2+1:end) = repmat(matrix(:, 1, end), [1, y1Expansion, z2Expansion]); matrixExpanded(x1:x2, y2+1:end, z2+1:end) = repmat(matrix(:, end, end), [1, y2Expansion, z2Expansion]); - + % Extend corner values. matrixExpanded(1:x1-1, 1:y1-1, 1:z1-1) = matrix(1, 1, 1)*ones(x1Expansion, y1Expansion, z1Expansion, dataType); matrixExpanded(1:x1-1, y2+1:end, 1:z1-1) = matrix(1, end, 1)*ones(x1Expansion, y2Expansion, z1Expansion, dataType); diff --git a/+kwave/+tutorials/+initialvalueproblems/AcosuticSolverExamples.m b/+kwave/+tutorials/+initialvalueproblems/AcosuticSolverExamples.m deleted file mode 100644 index 3ffc06d6..00000000 --- a/+kwave/+tutorials/+initialvalueproblems/AcosuticSolverExamples.m +++ /dev/null @@ -1,291 +0,0 @@ -%% Acoustic Solver example -% -%% Overview -% This example demonstrates how to construct and run a variety of simple -% problems using the acoustic solver in k-Wave II. From constructing the -% grid, building the Acooustic Medium, setting initial conditions and -% running the code. -% -% * -% * -% -% Within this example are commented alternative versions, marked by -% indents. These can be uncommented or commented out respectively. -% This Example is broken down into the following Sections. -% * Setup -% * Running the AcousticSolver and outputs -% * Including Absorption -% -% See Also: -% * kwave.tutorials.initialvalueproblems.homogeneousMedium2D -% - -%% Setup -% As with the other Initial Value Problem solvers in k-Wave II the -% AcousticSolver requires four main inputs. -% * The Grid -% * The Medium -% * The Source -% * The Sensors (optional) -% In this tutorial only the first three are covered. -% -% First we required access to the classes and functions of the k-Wave II -% toolbox. - -clearvars; -import kwave.toolbox.* - -% The grid is defined by calling the Grid(Inputs) function, this will -% define the computational space the problem exists on. The inputs for the -% Grid are: -% * The number of grid points -% * The distance between grid points in meters [m] -% * Size of the PML (optional) -% Example A gives a 1D domain without a PML -% Example B uses a 2D domain with different spacing between gridpoints -% Example C uses a 3D domain with the same size grid spacing in each -% dimension. -% Example D uses a 2D domain with a PML only in the y direction. -% Example E uses a 3D domain with a PML of the same size in each -% dimension. - -% ========================================================================= - - % A) - Nx=128; % Number of grid points - dx=1e-3; % Distance between grid points [m] - kgrid = Grid(Nx, dx); % Construct the grid. - % - - % B) - % Nx=128; % Number of x grid points - % Ny=54; % Number of y grid points - % dx=1e-3; % Distance between grid points in the first dimension [m] - % dy=5e-4; % Distance between grid points in the second dimension [m] - % kgrid = Grid([Nx,Ny], [dx,dy]); % Construct the grid. - % - - % C) - % Nx=128; % Number of x grid points - % Ny=54; % Number of y grid points - % Nz=129; % Number of z grid points - % dx=1e-3; % Distance between grid points in any dimension [m] - % kgrid = Grid([Nx,Ny, Nz], dx); % Construct the grid. - % - - % D) - % PMLx=0; % Number of Grid points either side of the domain for the PML in the x direction - % PMLy=20; % Number of Grid points either side of the domain for the PML in the y direction - % kgrid = Grid([128,128], 1e-3,[PMLx,PMLy]); % Construct the grid. - % - - % E) - % kgrid = Grid([128,64,64], 1e-3,20); % Construct the grid. - % - -% ========================================================================= - -% With the grid we are now required to construct the Medium and the Source. -% In k-wave-II for the AcousticSolver the medium can be constructed in two -% ways, through the AcousticMedium class, or through the Medium class. We -% will only cover the former in this example. -% The Acoustic Medium Class is constructed by calling it with the grid. -% Note that this call is the same irrespective of the specific grid -% properties. - -medium=AcousticMedium(kgrid); - -% In order to run the AcousticSolver two properties must be defined within -% the AcousticMedium. The Sound Speed. And the Mean density. These can be -% called either as constants across the entire domain, or defined on the -% grid for this example we will remain homogeneous. As such the following -% Commands can either be called - -c0=1500; % meters per second. [m s^{-1}] -rho0=100; % kilogram per meter^3 [kg m^{-3}] - -% ========================================================================= - - % - medium.soundSpeed=c0; % Sets the sound speed everywhere. - medium.density=rho0; % Sets the density everywhere. - % - - % - % medium.soundSpeed=ones(kgrid.gridSize)*c0; - % medium.density=ones(kgrid.gridSize)*rho0; - % - -% ========================================================================= - -% If you wish to consider an inhomogeneous domain the values of the -% soundSpeed and the Density must be changed directly and take the size of -% the grid. - -% For the AcousticSource there is only one property that is required. The -% Initial Pressure. This is also defined on the grid. - -source=AcousticSource(kgrid); - -% The initial pressure is set manually. In this case we will set it to take -% the form of a bell curve. - -source.initialPressure=exp( -(kgrid.x.^2 + kgrid.y.^2 + kgrid.z.^2)/ (10*kgrid.dx^2)); - -% It is worth noting that the commands kgrid.x, kgrid.y and kgrid.z are -% defined irrespective of the grid dimensions (1,2,3), and these will all -% be either scalar or the size of the grid. -% -% As an additional optionthe initial Velocity can also be defined. In -% general the problem defaults the initial Veelocity to be 0 at time t=0. -% -% If a velocity profile is given it is given as a gridField Vector, that is -% a 4D Array of size (kgrid.Nx,kgrid.Ny,Kgrid.Nz,kgrid.dimensions). Ny and -% Nz default to 1 when the problem is only 1D, or 2D for Nz. -% kgrid.dimensions takes the values 1,2 or 3, and indicate the variable in -% that dimensional direction. - -% ========================================================================= - - % - % source.initialVelocity=zeros([kgrid.gridSize,kgrid.dimensions]); % Ensures the initial velocity is the correct size. - % source.initialVelocity(:,:,:,1) = (1./(c0.*rho0)).*exp( -(kgrid.x.^2)/ (10*kgrid.dx^2)); % The velocity in the x direction - % - -% In this example the initial velocity in the y and z directions will -% be 0, such that the above lines of code will be valid irrespective of -% the starting domain. -% The initial velocity given must be defined as the velocity at each of -% the grid points at the time t=0, exactly as for the initialPressure. -% This is contrary to the outputs of the Acoustic Solver. This is -% detailed later. - -% ========================================================================= - -%% Running the AcousticSolver and outputs - -% In order to run the acoustiSolver we are required to initialise -% the AcousticSolver and call the run function. The input for the acoustic -% solver are, as previously highlighted; The grid. The medium. And the -% source. The Sensor is an option input not coered in this example. -% Additionally, settings may be given the AcousticSolver. We also do not -% cover this here. - -solver=AcousticSolver(kgrid,medium,source,[]); - -% To run the solver we need to only call the run function, which takes -% inputs of the Number of time steps and the size of the time steps. Also -% the CFL and endtime may be given, but this is not given here. - -% run is called as follows - -dt1=1e-7; -Nt1=200; - -% ========================================================================= - - % - solver.run(Nt=Nt1,dt=dt1); - % - -% Calling this function again with the same or different values of Nt and -% dt will continue the simulation from the end point of the previous call -% of run. For example the following will produce the same result as above; - - % - % solver.run(Nt=0,dt=dt1); - % solver.run(Nt=50,dt=dt1); - % solver.run(Nt=150,dt=dt1); - % solver.run(Nt=0,dt=dt1); - % - -% ========================================================================= -% -% Calling the run function with Nt=0 will not run any additional time -% steps, or if no time steps have been performed, will just initialise the -% problem. - -% The resulting AcousticSolver class, solver, will contain the following -% output properties of importance -% * solver.pressure % A grid size array -% * solver.densitySplit % A 4D gridfield vector -% * solver.velocity % A 4D gridfield vector -% * solver.timeArray % A vector of length (Nt+1) - -% These will be, respectively; -% The pressure perturbation at the end time (Nt*dt) at the grid points. -% The density at each point on the grid at the final time, split into directional -% co-ordinates. The true density perturbation is retuned by summing across the -% 4th dimension. -% The velocity in each of the co-ordinate directions, evaluated not on the -% grid points but at the offset half way points between the grid points in -% each direction. Additionally the velocity is evaluated at time -% (Nt+0.5)*dt. This is due to the staggered spacial and temporal grid used -% for each time step. -% The time points used in the compuation of the pressure, including time -% t=0. - -%% Including Absorption. -% -% in order to include abosrption in the problem there are three additional -% steps. -% * Define the absorption coefficient -% * Define the absorption power -% * Turn on the absorption within the solver. -% Just as before with the medium.soundSpeed and medium.density the -% abosrption coefficient is defined as a scalar on the grid, either as a -% constant value, or as a grid sized variable. - -alpha0= 0.5; -% ========================================================================= - - % - medium.absorptionCoeff=alpha0; - % - - % - % medium.absorptionCoeff=ones(kgrid.gridSize)*alpha0; - % - -% ========================================================================= - -% unlike the absorption coefficient the absorption power on the pother hand -% must be a scalar value. The code will not run correctly for a non-scalar -% absorptionPower due to limitations of the method. - -medium.absorptionPower=1.9; - -% As before we can now initialise the solver - -solver2=AcousticSolver(kgrid,medium,source,[]); - -% Before we run the simulation however we must tell the solver to consider -% the absorption. This is by setting solver.absorptionType - -% ========================================================================= - - % - solver2.absorptionType='on'; - % - - % - % solver.absorptionType="noAbsorption"; - % - - % - % solver.absorptionType="noDispersion"; - % - -% ========================================================================= - -% The options "noAbosrption" and "noDispersion" will also use a reduced -% form of the absorption, removing the terms relating to the Absorption and -% wave dispersion within the Treeby and Cox wave equations. -% In order to turn the absorption off, then simply set -% solver.absorptionType="off" which is default. - -% From here the simulation is run exactly as before - -solver2.run(Nt=Nt1,dt=dt1); - - \ No newline at end of file diff --git a/+kwave/+tutorials/+initialvalueproblems/Tutorials.m b/+kwave/+tutorials/+initialvalueproblems/Tutorials.m new file mode 100644 index 00000000..85ccc9ff --- /dev/null +++ b/+kwave/+tutorials/+initialvalueproblems/Tutorials.m @@ -0,0 +1,21 @@ +%% Tutorials +% * |t01_AcousticSolverExamples1D| +% * |t02_MoreAcousticSolverExamples1D| +% * |t03_AcousticSolverExamples2D| +% * |t04_MoreAcousticSolverExamples2D| + +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . diff --git a/+kwave/+tutorials/+initialvalueproblems/homogeneousMedium2D.m b/+kwave/+tutorials/+initialvalueproblems/homogeneousMedium2D.m deleted file mode 100644 index 4a03ff16..00000000 --- a/+kwave/+tutorials/+initialvalueproblems/homogeneousMedium2D.m +++ /dev/null @@ -1,42 +0,0 @@ -%% Initial value problem example -% -%% Overview -% This example demonstrates how to run a simple initial value problem using -% k-Wave II. -% -% * -% * -% -%% Setup -% k-Wave uses package folders to separate the toolbox, examples, tests, and -% utilities into different namespaces. To use the classes and functions -% within the toolbox, the package name prefix |kwave.toolbox| must be -% included before the class or function name, for example, -% |kwave.toolbox.Grid|. To avoid needing to use the package prefix -% within a given m-file, the toolbox can be imported as shown below. - -clearvars; -import kwave.toolbox.* - -%% Defining the grid -% The simulation functions in k-Wave require four inputs. These define the -% properties of the computational grid, the material properties of the -% medium, the properties and locations of any acoustic sources, and the -% properties and locations of the sensor points used to record the -% evolution of the pressure and velocity fields over time. -% -% Starting with the computational grid, the simulations are performed on a -% regular Cartesian mesh (for users with a background in finite-element -% methods, this is analogous to a structured mesh containing identical -% rectangular elements). The medium discretisation is defined using the -% |Grid| class. Both the total number of grid points |[Nx, Ny]| as -% well as the spacing between the grid points |dx| are used to compute the -% discretisation, and an object of the Grid class is returned. -% -% As the numerical techniques used in k-Wave are based heavily on the fast -% Fourier transform (FFT), the simulations will be fastest when the number -% of grid points in each direction is given by a power of two or has small -% prime factors. The prime factors for a particular grid can be computed -% using |kgrid.highestPrimeFactors|. - -kgrid = Grid([128, 128], 0.1e-3); diff --git a/+kwave/+tutorials/+initialvalueproblems/t01_AcousticSolverExamples1D.m b/+kwave/+tutorials/+initialvalueproblems/t01_AcousticSolverExamples1D.m new file mode 100644 index 00000000..701d885e --- /dev/null +++ b/+kwave/+tutorials/+initialvalueproblems/t01_AcousticSolverExamples1D.m @@ -0,0 +1,179 @@ +%% One-Dimensional Acoustic Solver Example +% +%% Overview +% This simple starter example demonstrates how to construct and run +% one-dimensional acoustic simulations using AcousticSolver. +% +% * To open the file in the MATLAB Editor: +% |edit('kwave.tutorials.initialvalueproblems.t01_AcousticSolverExamples1D.m')| +% * To run the file in MATLAB: +% |run('kwave.tutorials.initialvalueproblems.t01_AcousticSolverExamples1D.m')| +% +% See Also: +% +% * |kwave.tutorials.initialvalueproblems| + +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + + +%% Preliminaries +% +% This clears the workspace of any old variables +% +% clearvars; +% +% Import the k-Wave-II toolbox +% +% import kwave.toolbox.* + +clearvars +import kwave.toolbox.* + +%% Define a grid +% +% Nx = 128; % Number of grid points +% dx = 1e-3; % Grid spacing [m] +% kgrid = Grid(Nx, dx); % Create a Grid object + +Nx = 128; % Number of grid points +dx = 1e-3; % Grid spacing [m] +kgrid = Grid(Nx, dx); % Create a Grid object + +%% Define the acoustic properties of the medium +% +% medium = AcousticMedium(kgrid); % Create an AcousticMedium object +% medium.soundSpeed = 1500; % Set a constant sound speed [m/s] +% medium.density = 1000; % Set a constant density [kg/m^3] + +medium = AcousticMedium(kgrid); % Create an AcousticMedium object +medium.soundSpeed = 1500; % Set a constant sound speed [m/s] +medium.density = 1000; % Set a constant density [kg/m^3] + +%% Define an acoustic source +% +% Create an AcousticSource object +% +% source = AcousticSource(kgrid); +% +% In this example, an initial condition will be used to instigate the +% acoustic wave. Initial conditions are defined in the AcousticSource +% class, despite being distinct things mathematically, as they can give +% rise to an acoustic disturbance. Here the initial acoustic pressure is +% chosen to be a Gaussian. +% +% source.initialPressure = exp( -kgrid.x.^2 / (10*kgrid.dx^2) ); + +source = AcousticSource(kgrid); +source.initialPressure = exp( -kgrid.x.^2 / (10*kgrid.dx^2) ); + +%% Run the simulation +% +% To create an AcousticSolver object requires the other objects defined +% above: kgrid, medium and source. (An AcousticSensor object can be used as +% the fourth input, but if there are no sensors then it can be omitted, as +% here.) +% +% solver = AcousticSolver(kgrid,medium,source,[]); +% +% Define the time variables +% +% dt1 = 1e-7; % timestep size [s] +% Nt1 = 200; % Number of timesteps (not including time zero) +% +% Run the solver +% +% solver.run(Nt=Nt1, dt=dt1); +% +% Calling 'run' again will continue the simulation from the end time of the +% previous run. For example, the following command will take the simulation +% to timestep 250: +% +% solver.run(Nt=50, dt=dt1); +% +% The same result could have been achieved in one go using: +% +% solver = AcousticSolver(kgrid,medium,source,[]); +% solver.run(Nt=250,dt=dt1); +% +% Calling the run function with Nt=0 will not run any additional time +% steps. If no time steps have been performed, it will just initialise the +% problem with the initial conditions at time 0: +% +% solver.run(Nt=0, dt=dt1); + +solver = AcousticSolver(kgrid,medium,source,[]); + +dt1 = 1e-7; % timestep size [s] +Nt1 = 200; % Number of timesteps (not including time zero) + +solver.run(Nt=Nt1, dt=dt1); +solver.run(Nt=50, dt=dt1); + +%% The simulation output +% +% Following execution of run, the solver object will contain the following +% output properties: +% +% * |solver.pressure| : A grid-sized array +% * |solver.densitySplit| : A 4D gridfield vector +% * |solver.velocity| : A 4D gridfield vector +% * |solver.timeArray| : A vector of length (Nt+1) +% +% These are, respectively: +% +% * The acoustic pressure field on the grid points at the end time Nt*dt. +% * The acoustic density field on the grid points at the final time, split +% into directional coordinates (for the sake of the PML). The true +% density field can be found by summing across the 4th dimension. +% * The acoustic particle velocity in each of the coordinate directions, +% evaluated on the staggered grid at time (Nt+0.5)*dt (because of the +% use of both space and time staggering). +% * The time points used in the simulation from time 0 to Nt*dt +% +% Plot the initial and final acoustic pressure fields +% +% figure +% subplot(3,1,1) +% plot(kgrid.xVec*1e3,source.initialPressure) +% xlabel('x [mm]') +% title('Initial acoustic pressure') +% +% subplot(3,1,2) +% plot(kgrid.xVec*1e3,solver.pressure) +% xlabel('x [mm]') +% title('Acoustic pressure at time Nt*dt') +% +% subplot(3,1,3) +% plot(kgrid.xVec*1e3,solver.velocity) +% xlabel('x [mm]') +% title('Particle velocity at time Nt*dt') + +figure +subplot(3,1,1) +plot(kgrid.xVec*1e3,source.initialPressure) +xlabel('x [mm]') +title('Initial acoustic pressure') + +subplot(3,1,2) +plot(kgrid.xVec*1e3,solver.pressure) +xlabel('x [mm]') +title('Acoustic pressure at time Nt*dt') + +subplot(3,1,3) +plot(kgrid.xVec*1e3,solver.velocity) +xlabel('x [mm]') +title('Particle velocity at time Nt*dt') diff --git a/+kwave/+tutorials/+initialvalueproblems/t02_MoreAcousticSolverExamples1D.m b/+kwave/+tutorials/+initialvalueproblems/t02_MoreAcousticSolverExamples1D.m new file mode 100644 index 00000000..25e3b1b7 --- /dev/null +++ b/+kwave/+tutorials/+initialvalueproblems/t02_MoreAcousticSolverExamples1D.m @@ -0,0 +1,245 @@ +%% More 1D Acoustic Solver Examples +% +%% Overview +% More 1D examples, following on from |t01_AcousticSolverExamples1D|, +% further capabilities of AcousticSolver in one-dimension. +% +% * To open the file in the MATLAB Editor: +% |edit('kwave.tutorials.initialvalueproblems.t02_MoreAcousticSolverExamples1D.m')| +% * To run the file in MATLAB: +% |run('kwave.tutorials.initialvalueproblems.t02_MoreAcousticSolverExamples1D.m')| +% +% See Also: +% +% * |kwave.tutorials.initialvalueproblems.t01_AcousticSolverExamples1D.m| + +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + + +%% Preliminaries +% +% This clears the workspace of any old variables +% +% clearvars; +% +% Import the k-Wave-II toolbox +% +% import kwave.toolbox.* + +clearvars; % Clear the workspace of old variables +import kwave.toolbox.* % Import the k-Wave-II toolbox + + +%% Define a grid +% +% Nx = 128; % Number of grid points +% dx = 1e-3; % Grid spacing [m] +% pmlSize = 20; % Thickness of the PML (Perfectly Matched Layer absorbing boundary) +% kgrid = Grid(Nx, dx, pmlSize); % Create a Grid object + +Nx = 128; % Number of grid points +dx = 1e-3; % Grid spacing [m] +pmlSize = 20; % Thickness of the PML (Perfectly Matched Layer absorbing boundary) +kgrid = Grid(Nx, dx, pmlSize); % Create a Grid object + + +%% Define heterogeneous acoustic properties, and include acoustic absorption +% +% Create an AcousticMedium object +% +% medium = AcousticMedium(kgrid); +% +% Define a spatially-varying sound speed (here a step change) +% +% c0 = 1500*ones(medium.gridSize); +% c0(1:end/2) = 1400; +% medium.soundSpeed = c0; % [m/s] +% +% Define a spatially-varying ambient density (also a step change) +% +% rho0 = 1000*ones(medium.gridSize); +% rho0(1:end/2) = 940; +% medium.density = rho0; % [kg/m^3] +% +% Define the acoustic absorption coefficient prefactor, the value for +% alpha0 in the expression for the absorption coefficient: +% alpha = alpha0 * f^y, where f is the frequency in MHz and y is the power +% law exponent (absorptionPower, which must be scalar). +% +% medium.absorptionCoeff = 0.5; % [dB/cm/MHz^y] +% medium.absorptionPower = 1.9; +% +% Note that when using absorption, the solver must be told: +% +% solver = AcousticSolver(kgrid,medium,source,sensor); +% solver.absorptionType='on'; + +medium = AcousticMedium(kgrid); + +% Define a sound speed +c0 = 1500*ones(medium.gridSize); +c0(1:end/2) = 1400; +medium.soundSpeed = c0; % [m/s] + +% Define a ambient density +rho0 = 1000*ones(medium.gridSize); +rho0(1:end/2) = 940; +medium.density = rho0; % [kg/m^3] + +% Define the acoustic absorption +medium.absorptionCoeff = 0.5; % [dB/cm/MHz^y] +medium.absorptionPower = 1.9; + + +%% Define an acoustic source +% +% Create an AcousticSource object +% +% source = AcousticSource(kgrid); +% +% Define an initial acoustic pressure distribution +% +% offset = floor(kgrid.Nx/4); +% source.initialPressure = exp( -(kgrid.x - offset*kgrid.dx).^2 / (10*kgrid.dx^2) ); + +source = AcousticSource(kgrid); +offset = floor(kgrid.Nx/4); +source.initialPressure = exp( -(kgrid.x - offset*kgrid.dx).^2 / (10*kgrid.dx^2) ); + + +%% Define an acoustic sensor +% +% Create an AcousticSensor object +% +% sensor = AcousticSensor(kgrid); +% +% Define a binary mask. The solver will return the field variables at the +% grid locations marked by 1 in the mask. +% +% sensor.mask = zeros(kgrid.gridSize); +% sensor.mask(end - floor(Nx/8)) = 1; +% +% Record at every other timestep +% +% sensor.timeStepSpacing = 2; + +sensor = AcousticSensor(kgrid); +sensor.mask = zeros(kgrid.gridSize); +sensor.mask(end - floor(Nx/8)) = 1; + +% Record at every other timestep +sensor.timeStepSpacing = 2; + + +%% Run the simulation +% +% Create an AcousticSolver object +% +% solver = AcousticSolver(kgrid,medium,source,sensor); +% +% Turn on the absorption +% +% solver.absorptionType='on'; +% +% Define the CFL (Courant-Friedrichs-Lewy) number and endTime allow the +% timestep to be chosen automatically +% +% cfl = 0.2; +% endTime = 0.65 * kgrid.dx*kgrid.Nx / min(medium.soundSpeed(:)); +% +% Run the solver +% +% solver.run(CFL=cfl, EndTime=endTime); + +solver = AcousticSolver(kgrid,medium,source,sensor); +solver.absorptionType='on'; + +% Timestep chosen automatically +cfl = 0.2; +endTime = 0.65 * kgrid.dx*kgrid.Nx / min(medium.soundSpeed(:)); + +% Run the solver +solver.run(CFL=cfl, EndTime=endTime); + + +%% The simulation output +% +% Plot the initial and final acoustic pressure fields +% +% figure +% +% subplot(3,2,1) +% plot(kgrid.xVec*1e3,medium.soundSpeed) +% xlabel('x [mm]') +% title('Sound speed') +% +% subplot(3,2,3) +% plot(kgrid.xVec*1e3,medium.density) +% xlabel('x [mm]') +% title('Ambient density') +% +% subplot(3,2,5) +% stem(kgrid.xVec*1e3,sensor.mask) +% xlabel('x [mm]') +% title('Sensor mask') +% +% subplot(3,2,2) +% plot(kgrid.xVec*1e3,source.initialPressure) +% xlabel('x [mm]') +% title('Initial acoustic pressure') +% +% subplot(3,2,4) +% plot(kgrid.xVec*1e3,solver.pressure) +% xlabel('x [mm]') +% title('Acoustic pressure at final time') +% +% subplot(3,2,6) +% plot(sensor.times*1e6,sensor.pressure,'.-') +% xlabel('time [\mus]') +% title('Pressure time series recorded at sensor point') + +figure + +subplot(3,2,1) +plot(kgrid.xVec*1e3,medium.soundSpeed) +xlabel('x [mm]') +title('Sound speed') + +subplot(3,2,3) +plot(kgrid.xVec*1e3,medium.density) +xlabel('x [mm]') +title('Ambient density') + +subplot(3,2,5) +stem(kgrid.xVec*1e3,sensor.mask) +xlabel('x [mm]') +title('Sensor mask') + +subplot(3,2,2) +plot(kgrid.xVec*1e3,source.initialPressure) +xlabel('x [mm]') +title('Initial acoustic pressure') + +subplot(3,2,4) +plot(kgrid.xVec*1e3,solver.pressure) +xlabel('x [mm]') +title('Acoustic pressure at final time') + +subplot(3,2,6) +plot(sensor.times*1e6,sensor.pressure,'.-') +xlabel('time [\mus]') +title('Pressure time series recorded at sensor point') diff --git a/+kwave/+tutorials/+initialvalueproblems/t03_AcousticSolverExamples2D.m b/+kwave/+tutorials/+initialvalueproblems/t03_AcousticSolverExamples2D.m new file mode 100644 index 00000000..89ddfc3f --- /dev/null +++ b/+kwave/+tutorials/+initialvalueproblems/t03_AcousticSolverExamples2D.m @@ -0,0 +1,266 @@ +%% Two-Dimensional Acoustic Solver examples +% +%% Overview +% This example demonstrates the AcousticSolver with 2D examples. +% +% * To open the file in the MATLAB Editor: +% |edit('kwave.tutorials.initialvalueproblems.t03_AcousticSolverExamples2D.m')| +% * To run the file in MATLAB: +% |run('kwave.tutorials.initialvalueproblems.t03_AcousticSolverExamples2D.m')| +% +% See Also: +% +% * |kwave.tutorials.initialvalueproblems.t01_AcousticSolverExamples1D| +% * |kwave.tutorials.initialvalueproblems.t02_MoreAcousticSolverExamples1D| + +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + + +%% Preliminaries +% +% This clears the workspace of any old variables +% +% clearvars; +% +% Import the k-Wave-II toolbox +% +% import kwave.toolbox.* + +clearvars; % Clear the workspace of old variables +import kwave.toolbox.* % Import the k-Wave-II toolbox + + +%% Define a 2D grid +% +% Nx = 128; % Number of grid points in x-direction +% Ny = 128; % Number of grid points in y-direction +% dx = 1e-3; % Grid spacing in x-direction [m] +% dy = 1e-3; % Grid spacing in y-direction [m] +% pmlSizex = 20; % Thickness of the PML (Perfectly Matched Layer absorbing boundary) +% pmlSizey = 20; % Thickness of the PML (Perfectly Matched Layer absorbing boundary) +% +% Create a Grid object +% +% kgrid = Grid([Nx Ny], [dx dy], [pmlSizex pmlSizey]); + +Nx = 128; % Number of grid points in x-direction +Ny = 128; % Number of grid points in y-direction +dx = 1e-3; % Grid spacing in x-direction [m] +dy = 1e-3; % Grid spacing in y-direction [m] +pmlSizex = 20; % Thickness of the PML (Perfectly Matched Layer absorbing boundary) +pmlSizey = 20; % Thickness of the PML (Perfectly Matched Layer absorbing boundary) + +% Create a Grid object +kgrid = Grid([Nx Ny], [dx dy], [pmlSizex pmlSizey]); + + +%% Define acoustic properties, and include acoustic absorption +% +% Create an AcousticMedium object +% +% medium = AcousticMedium(kgrid); +% +% Define a spatially-varying sound speed (here a step change) +% +% c0 = 1500*ones(medium.gridSize); +% c0(1:end/2,:) = 2200; +% medium.soundSpeed = c0; % [m/s] +% +% Define a spatially-varying ambient density (also a step change) +% +% rho0 = 1000*ones(medium.gridSize); +% rho0(1:end/2,:) = 1500; +% medium.density = rho0; % [kg/m^3] +% +% Define the acoustic absorption coefficient prefactor, the value for +% alpha0 in the expression for the absorption coefficient: +% alpha = alpha0 * f^y, where f is the frequency in MHz and y is the power +% law exponent (absorptionPower, which must be scalar). +% +% medium.absorptionCoeff = 0.5; % [dB/cm/MHz^y] +% medium.absorptionPower = 1.9; + +medium = AcousticMedium(kgrid); + +% Define sound speed +c0 = 1500*ones(medium.gridSize); +c0(1:end/2,:) = 2200; +medium.soundSpeed = c0; % [m/s] + +% Define ambient density +rho0 = 1000*ones(medium.gridSize); +rho0(1:end/2,:) = 1500; +medium.density = rho0; % [kg/m^3] + +% Define the acoustic absorption +medium.absorptionCoeff = 0.5; % [dB/cm/MHz^y] +medium.absorptionPower = 1.9; + + +%% Define an acoustic source +% +% Create an AcousticSource object +% +% source = AcousticSource(kgrid); +% +% Define an initial acoustic pressure distribution +% +% offset = 0.25*kgrid.dx*Nx; +% r = hypot(kgrid.x - offset,kgrid.y); % radial coordinate +% source.initialPressure = exp( -r.^2 / (10*kgrid.dx^2) ); + +source = AcousticSource(kgrid); +offset = 0.25*kgrid.dx*Nx; +r = hypot(kgrid.x - offset,kgrid.y); % radial coordinate +source.initialPressure = exp( -r.^2 / (10*kgrid.dx^2) ); + + +%% Define an acoustic sensor +% +% Create an AcousticSensor object +% +% sensor = AcousticSensor(kgrid); +% +% Define a binary mask. The solver will return the field variables at the +% grid locations marked by 1 in the mask, here a line array +% +% sensor.mask = zeros(kgrid.gridSize); +% sensor.mask(end - floor(Nx/8),:) = 1; + +sensor = AcousticSensor(kgrid); + +% Define a binary mask +sensor.mask = zeros(kgrid.gridSize); +sensor.mask(end - floor(Nx/8),:) = 1; + + +%% Run the simulation +% +% Create an AcousticSolver object +% +% solver = AcousticSolver(kgrid,medium,source,sensor); +% +% Turn on the absorption +% +% solver.absorptionType='on'; +% +% Define the CFL (Courant-Friedrichs-Lewy) number and endTime allow the +% timestep to be chosen automatically +% +% cfl = 0.2; +% endTime = 0.5 * kgrid.dx*kgrid.Nx / min(medium.soundSpeed(:)); +% +% Run the solver +% +% solver.run(CFL=cfl, EndTime=endTime); + +solver = AcousticSolver(kgrid,medium,source,sensor); + +% Turn on the absorption +solver.absorptionType='on'; + +% Define timestep automatically +cfl = 0.2; +endTime = 0.5 * kgrid.dx*kgrid.Nx / min(medium.soundSpeed(:)); + +% Run the solver +solver.run(CFL=cfl, EndTime=endTime); + + +%% Visualisations +% +% figure +% +% subplot(2,2,1) +% imagesc(medium.soundSpeed) +% axis equal +% colorbar +% xlabel('x [m]') +% ylabel('y [m]') +% title('Sound speed [m/s]') +% +% subplot(2,2,2) +% imagesc(medium.density) +% axis equal +% colorbar +% xlabel('x [m]') +% ylabel('y [m]') +% title('Density [kg/m^3]') +% +% subplot(2,2,3) +% imagesc(source.initialPressure) +% axis equal +% colorbar +% xlabel('x [m]') +% ylabel('y [m]') +% title('Initial acoustic pressure') +% +% subplot(2,2,4) +% imagesc(solver.pressure) +% axis equal +% colorbar +% xlabel('x [m]') +% ylabel('y [m]') +% title('Final acoustic pressure') +% +% figure +% +% imagesc(sensor.pressure) +% xlabel('time steps') +% ylabel('sensor position') +% title('Pressure time series recorded at the sensor array') + +figure + +subplot(2,2,1) +imagesc(medium.soundSpeed) +axis equal +colorbar +xlabel('x [m]') +ylabel('y [m]') +title('Sound speed [m/s]') + +subplot(2,2,2) +imagesc(medium.density) +axis equal +colorbar +xlabel('x [m]') +ylabel('y [m]') +title('Density [kg/m^3]') + +subplot(2,2,3) +imagesc(source.initialPressure) +axis equal +colorbar +xlabel('x [m]') +ylabel('y [m]') +title('Initial acoustic pressure') + +subplot(2,2,4) +imagesc(solver.pressure) +axis equal +colorbar +xlabel('x [m]') +ylabel('y [m]') +title('Final acoustic pressure') + +figure + +imagesc(sensor.pressure) +xlabel('time steps') +ylabel('sensor position') +title('Pressure time series recorded at the sensor array') diff --git a/+kwave/+tutorials/+initialvalueproblems/t04_MoreAcousticSolverExamples2D.m b/+kwave/+tutorials/+initialvalueproblems/t04_MoreAcousticSolverExamples2D.m new file mode 100644 index 00000000..dc74ba37 --- /dev/null +++ b/+kwave/+tutorials/+initialvalueproblems/t04_MoreAcousticSolverExamples2D.m @@ -0,0 +1,295 @@ +%% More Two-Dimensional Acoustic Solver examples +% +%% Overview +% More 2D examples, following on from |t03_AcousticSolverExamples2D|. +% +% * To open the file in the MATLAB Editor: +% |edit('kwave.tutorials.initialvalueproblems.t04_MoreAcousticSolverExamples2D.m')| +% * To run the file in MATLAB: +% |run('kwave.tutorials.initialvalueproblems.t04_MoreAcousticSolverExamples2D.m')| +% +% See Also: +% * kwave.tutorials.initialvalueproblems.t03_MoreAcousticSolverExamples2D + +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + + +%% Preliminaries +% +% This clears the workspace of any old variables +% +% clearvars; +% +% Import the k-Wave-II toolbox +% +% import kwave.toolbox.* + +clearvars; % Clear the workspace of old variables +import kwave.toolbox.* % Import the k-Wave-II toolbox + +%% Define a 2D grid +% +% Nx = 128; % Number of grid points in x-direction +% Ny = 128; % Number of grid points in y-direction +% dx = 1e-3; % Grid spacing in x-direction [m] +% dy = 1e-3; % Grid spacing in y-direction [m] +% pmlSizex = 20; % Thickness of the PML (Perfectly Matched Layer absorbing boundary) +% pmlSizey = 20; % Thickness of the PML (Perfectly Matched Layer absorbing boundary) +% +% Create a Grid object +% +% kgrid = Grid([Nx Ny], [dx dy], [pmlSizex pmlSizey]); + +Nx = 128; % Number of grid points in x-direction +Ny = 128; % Number of grid points in y-direction +dx = 1e-3; % Grid spacing in x-direction [m] +dy = 1e-3; % Grid spacing in y-direction [m] +pmlSizex = 20; % Thickness of the PML (Perfectly Matched Layer absorbing boundary) +pmlSizey = 20; % Thickness of the PML (Perfectly Matched Layer absorbing boundary) + +% Create a Grid object +kgrid = Grid([Nx Ny], [dx dy], [pmlSizex pmlSizey]); + +%% List materials +% +% Create a Materials object (essentially a database of material types) +% +% materials = Materials(); +% +% Add new materials to the Materials object +% +% idx1 = materials.addMaterial('softTissue1', struct('soundSpeed',1540,'density',1100)); +% idx2 = materials.addMaterial('softTissue2', struct('soundSpeed',1440,'density',990)); +% +% Show a table of all the materials currently stored +% +% materials_table = materials.listMaterials(); +% disp(materials_table); + +materials = Materials(); + +% Add new materials to the Materials object +idx1 = materials.addMaterial('softTissue1', struct('soundSpeed',1540,'density',1100)); +idx2 = materials.addMaterial('softTissue2', struct('soundSpeed',1440,'density',990)); + +% Show a table of all the materials currently stored +materials_table = materials.listMaterials(); +disp(materials_table); + +%% Define properties of material types +% +% Create a Medium object +% +% medium = Medium(kgrid, materials); +% +% Define map of material type indices (uint8) +% +% medium.materialIndexGrid = zeros(medium.gridSize, 'uint8'); % water +% medium.materialIndexGrid(end/4:end/2,:) = idx1; % softTissue1 +% medium.materialIndexGrid(end/2+1:end,:) = idx2; % softTissue2 + +% Create a Medium object +medium = Medium(kgrid, materials); + +% Define map of material type indices (uint8) +medium.materialIndexGrid = zeros(medium.gridSize, 'uint8'); % water +medium.materialIndexGrid(end/4:end/2,:) = idx1; % softTissue1 +medium.materialIndexGrid(end/2+1:end,:) = idx2; % softTissue2 + + +%% Visualize the sound speed and density maps +% +% Extract the sound speed and density maps +% +% c_map = medium.soundSpeed; % [m/s] +% rho_map = medium.density; % [kg/m^3] +% +% Plot the sound speed and density maps +% +% figure; +% subplot(1, 2, 1); +% imagesc(c_map); +% colorbar; +% title('Sound Speed Map (m/s)'); +% axis image; +% subplot(1, 2, 2); +% imagesc(rho_map); +% colorbar; +% title('Density Map (kg/m^3)'); +% axis image; + +c_map = medium.soundSpeed; % [m/s] +rho_map = medium.density; % [kg/m^3] + +figure; +subplot(1, 2, 1); +imagesc(c_map); +colorbar; +title('Sound Speed Map (m/s)'); +axis image; +subplot(1, 2, 2); +imagesc(rho_map); +colorbar; +title('Density Map (kg/m^3)'); +axis image; + + +%% Define an acoustic source +% +% Create an AcousticSource object +% +% source = AcousticSource(kgrid); +% +% Define an initial acoustic pressure distribution +% +% offset = 0.25*kgrid.dx*Nx; +% r = hypot(kgrid.x - offset,kgrid.y); % radial coordinate +% source.initialPressure = exp( -r.^2 / (10*kgrid.dx^2) ); + +source = AcousticSource(kgrid); +offset = 0.25*kgrid.dx*Nx; +r = hypot(kgrid.x - offset,kgrid.y); % radial coordinate +source.initialPressure = exp( -r.^2 / (10*kgrid.dx^2) ); + +%% Define an acoustic sensor +% +% Create an AcousticSensor object +% +% sensor = AcousticSensor(kgrid); +% +% Define a binary mask. The solver will return the field variables at the +% grid locations marked by 1 in the mask, here a line array +% +% sensor.mask = zeros(kgrid.gridSize); +% sensor.mask(end - floor(Nx/8),:) = 1; + +% Create an AcousticSensor object +sensor = AcousticSensor(kgrid); + +% Define a binary mask. The solver will return the field variables at the +% grid locations marked by 1 in the mask, here a line array +sensor.mask = zeros(kgrid.gridSize); +sensor.mask(end - floor(Nx/8),:) = 1; + +%% Run the simulation +% +% Create an AcousticSolver object +% +% solver = AcousticSolver(kgrid,medium,source,sensor); +% +% Define the CFL (Courant-Friedrichs-Lewy) number and endTime allow the +% timestep to be chosen automatically +% +% cfl = 0.2; +% endTime = 0.75 * kgrid.dx*kgrid.Nx / min(medium.soundSpeed(:)); +% +% Run the solver +% +% solver.run(CFL=cfl, EndTime=endTime); + +solver = AcousticSolver(kgrid,medium,source,sensor); + +% Define the timestep automatically +cfl = 0.2; +endTime = 0.75 * kgrid.dx*kgrid.Nx / min(medium.soundSpeed(:)); + +% Run the solver +solver.run(CFL=cfl, EndTime=endTime); + +%% Visualisations +% +% figure +% +% subplot(2,2,1) +% imagesc(medium.soundSpeed) +% axis equal +% colorbar +% xlabel('x [m]') +% ylabel('y [m]') +% title('Sound speed [m/s]') +% +% subplot(2,2,2) +% imagesc(medium.density) +% axis equal +% colorbar +% xlabel('x [m]') +% ylabel('y [m]') +% title('Density [kg/m^3]') +% +% subplot(2,2,3) +% imagesc(source.initialPressure) +% axis equal +% colorbar +% xlabel('x [m]') +% ylabel('y [m]') +% title('Initial acoustic pressure') +% +% subplot(2,2,4) +% imagesc(solver.pressure) +% axis equal +% colorbar +% xlabel('x [m]') +% ylabel('y [m]') +% title('Final acoustic pressure') +% +% figure +% +% imagesc(sensor.pressure) +% xlabel('time steps') +% ylabel('sensor position') +% title('Pressure time series recorded at the sensor array') + +figure + +subplot(2,2,1) +imagesc(medium.soundSpeed) +axis equal +colorbar +xlabel('x [m]') +ylabel('y [m]') +title('Sound speed [m/s]') + +subplot(2,2,2) +imagesc(medium.density) +axis equal +colorbar +xlabel('x [m]') +ylabel('y [m]') +title('Density [kg/m^3]') + +subplot(2,2,3) +imagesc(source.initialPressure) +axis equal +colorbar +xlabel('x [m]') +ylabel('y [m]') +title('Initial acoustic pressure') + +subplot(2,2,4) +imagesc(solver.pressure) +axis equal +colorbar +xlabel('x [m]') +ylabel('y [m]') +title('Final acoustic pressure') + +figure + +imagesc(sensor.pressure) +xlabel('time steps') +ylabel('sensor position') +title('Pressure time series recorded at the sensor array') diff --git a/+kwave/+utilities/GenerateDocumentation.m b/+kwave/+utilities/GenerateDocumentation.m deleted file mode 100644 index 7dbe0cc1..00000000 --- a/+kwave/+utilities/GenerateDocumentation.m +++ /dev/null @@ -1,283 +0,0 @@ -%% GenerateDocumentation -% *Package:* kwave.utilities -% -% Generate help documentation. -% -%% Syntax -% kwave.utilities.GenerateDocumentation -% -%% Description -% |GenerateDocumentation| is a static class that is called to generate the -% help files that appear in the MATLAB help browser. The documentation is -% generated in several stages: -% -% # The .m files in specified directories are converted directly to .html -% using the MATLAB function. Text should -% be written using MATLAB publishing markup, which is parsed as headings, -% code, etc. -% # The generated .html files are modified to add html links between -% related functions and classes. -% # A helptoc.xml file is automatically created, adding links to the -% generated .html files. -% # The documentation search database is created using -% . -% -% This class is provided for generating documentation during development, -% and for preparing the documentation packaged with a release. If you have -% downloaded a packaged release, there should be no need to call this -% class. However, if you have directly cloned the repository and are -% working with bleeding-edge changes, you can call |GenerateDocumentation| -% to (re-)generate the help files. - -classdef GenerateDocumentation - - properties(SetAccess=immutable, Hidden=true) - helpDir; - rootPath; - end - - % Constructor. - methods(Static) - function obj = GenerateDocumentation() - - % Setup paths. The toolbox root directory is two levels above - % +kwave/+utilities. Add back forward slash to directory if on - % linux or mac. - [mFilePath, ~, ~] = fileparts(mfilename('fullpath')); - mFilePathParts = split(mFilePath, filesep); - obj.rootPath = fullfile(mFilePathParts{1:end-2}); - if isunix - obj.rootPath = [filesep obj.rootPath]; - end - addpath(obj.rootPath); - obj.helpDir = fullfile(obj.rootPath, 'helpfiles'); - obj.createHelpDir; - - % Generate HTML files. - obj.generateHTML('/+kwave/+docfiles/+general'); - initialValueProblemsFilenames = obj.generateHTML('/+kwave/+tutorials/+initialvalueproblems', evalCode=true, showCode=true); - toolboxFilenames = obj.generateHTML('/+kwave/+toolbox'); - testFilenames = obj.generateHTML('/+kwave/+tests'); - unitTestFilenames = obj.generateHTML('/+kwave/+tests/+unit'); - utilityFilenames = obj.generateHTML('/+kwave/+utilities'); - developerFilenames = obj.generateHTML('/+kwave/+docfiles/+developer', showCode=true); - - % Build helptoc. - obj.helpTocStart; - obj.helpTocAddSection(initialValueProblemsFilenames, 'Initial Value Problems'); - obj.helpTocAddSection(toolboxFilenames, 'Toolbox Functions'); - obj.helpTocAddSection(testFilenames, 'Test Functions'); - obj.helpTocAddSection(unitTestFilenames, 'Unit Tests'); - obj.helpTocAddSection(utilityFilenames, 'Utility Functions'); - obj.helpTocAddSection(developerFilenames, 'Developer Documentation'); - obj.helpTocFinish; - - % Build searchable docs. - disp('Generating search database...'); - builddocsearchdb(obj.helpDir); - - end - end - - methods(Access=private, Hidden=true) - - % Create empty helpfiles directory. - function createHelpDir(obj) - if exist(obj.helpDir, 'file') - rmdir(obj.helpDir, 's'); - end - mkdir(obj.helpDir); - end - - % Convert m-files in specified directory to HTML using publish. - function mFilenames = generateHTML(obj, relativeFolder, options) - arguments - obj - relativeFolder - options.showCode = false; - options.evalCode = false; - end - - % Get namespace from folder - nameSpace = obj.folderToNamespace(relativeFolder); - - % Find all m-files contained in relativeFolder and - % sub-directories. - disp(['Generating HTML for ' relativeFolder '...']); - absolutePath = fullfile(obj.rootPath, relativeFolder); - mFilenames = dir(fullfile(absolutePath, '**/*.m')); - numFiles = length(mFilenames); - - % Initialise additional properties that we will set. - [mFilenames(:).isClassMethod] = deal(false); - [mFilenames(:).isClass] = deal(false); - [mFilenames(:).className] = deal(''); - [mFilenames(:).title] = deal(''); - [mFilenames(:).htmlFileName] = deal(''); - - % Loop over m-files. - for ind = 1:numFiles - - % Get filename without extension. - [~, filename, ~] = fileparts(mFilenames(ind).name); - - % Get relative folder (may be empty if in root folder). - mFileRelativeFolder = erase(mFilenames(ind).folder, absolutePath); - mFileRelativeFolder = erase(mFileRelativeFolder, filesep); - - % We need to treat class methods that are in separate files - % slightly differently, as these can only be compiled if we - % change directories. To do this, check if the m-file is in - % a class folder AND has a different name to the class. - % Otherwise, we need to prepend the namespace to the - % filename. If there is a nested namespace, this also needs - % prepending. - if obj.isClass(mFileRelativeFolder, filename) - mFilenames(ind).isClass = true; - end - - if obj.isClassMethod(mFileRelativeFolder, filename) - mFilenames(ind).isClassMethod = true; - mFilenames(ind).className = [mFileRelativeFolder(2:end) '.m']; % Convert folder name to class name by removing the leading "@" character. - cd(mFilenames(ind).folder); - filename = [filename '.m']; %#ok - else - % Not a class or class method - filename = [nameSpace filename]; %#ok - end - - % Print details of conversions. - disp(['Converting ', filename, ' to HTML (', int2str(ind), '/', int2str(numFiles), ')']); - - % Extract title used for entry in helptoc.xml from first - % line of code documentation. - mFilenames(ind).title = kwave.utilities.parseTitle(fullfile(mFilenames(ind).folder, mFilenames(ind).name)); - - % Publish. - htmlFile = publish(filename, ... - 'format', 'html', ... - 'outputDir', obj.helpDir, ... - 'evalCode', options.evalCode, ... - 'showCode', options.showCode); - - % Rename to include classname if a class method. - if mFilenames(ind).isClassMethod - [~, htmlFilename, ~] = fileparts(htmlFile); - [~, className, ~] = fileparts(mFilenames(ind).className); - newHtmlFile = fullfile(obj.helpDir, [className '-' htmlFilename '.html']); - movefile(htmlFile, newHtmlFile); - htmlFile = newHtmlFile; - end - [~, fileName, ext] = fileparts(htmlFile); - mFilenames(ind).htmlFileName = [fileName, ext]; - - % Change back to root directory. - cd(obj.rootPath); - - end - - % Add relative links to class methods from class documentation. - for ind1 = 1:numFiles - for ind2 = 1:numFiles - if mFilenames(ind1).isClass && mFilenames(ind2).isClassMethod && strcmp(mFilenames(ind2).className, mFilenames(ind1).name) - - disp(['Replacing links to method ', mFilenames(ind2).name, ' from class ', mFilenames(ind1).name]); - [~, methodName, ~] = fileparts(mFilenames(ind2).name); - htmlFileName = mFilenames(ind1).htmlFileName; - htmlFile = fullfile(obj.helpDir, htmlFileName); - - % Read in HTML file. - fid = fopen(htmlFile, 'r'); - fileContents = fread(fid, '*char'); - fclose(fid); - - % Replace links, and save to HTML file replacing - % contents. The |methodName| syntax is published as - % methodName. The html flags are included - % in the search to avoid adding links to code - % snippets. - fileContents = strrep(fileContents.', ... - ['' methodName ''], ... - ['' obj.generateLink(mFilenames(ind2).htmlFileName, methodName) '']); - fid = fopen(htmlFile, 'w'); - fprintf(fid, '%s', fileContents.'); - fclose(fid); - - end - end - end - end - - % Given a directory (e.g. '/+kwave/+utils'), convert to the - % corresponding namespace (e.g. 'kwave.utils') - function nameSpace = folderToNamespace(~, folder) - - nameSpace = replace(folder, '/+', '.'); - - % Strip any leading . - if strcmp(nameSpace(1), '.') - nameSpace = nameSpace(2:end); - end - - % Add a trailing . - if ~isempty(nameSpace) - nameSpace = [nameSpace, '.']; - end - - end - - % Given a folder (e.g. '@Grid') and a filename (e.g. - % 'Grid.m'), return true if the file represents the class - % itself (as opposed to a class method) - function isClass = isClass(~, mFileRelativeFolder, filename) - isClass = ~isempty(mFileRelativeFolder) && strcmp(extractAfter(mFileRelativeFolder, 1), filename); - end - - % Given a folder (e.g. '@Grid') and a filename (e.g. - % 'validateSize.m'), return true if the file represents a class - % method - function isClassMethod = isClassMethod(obj, mFileRelativeFolder, filename) - isClassMethod = ~isempty(mFileRelativeFolder) && ~obj.isClass(mFileRelativeFolder, filename); - end - - % Start generation of helptoc.xml. - function helpTocStart(obj) - disp('Generating helptoc.xml...'); - obj.addToXML(''); - obj.addToXML(''); - obj.addToXML('k-Wave II'); - end - - % Add links to html for all functions, excluding class methods. - function helpTocAddSection(obj, mFilenames, heading) - obj.addToXML(['' heading]); - for ind = 1:length(mFilenames) - if ~mFilenames(ind).isClassMethod - obj.addToXML(['' mFilenames(ind).title '']); - end - end - obj.addToXML(''); - end - - % Finish generation of helptoc.xml. - function helpTocFinish(obj) - obj.addToXML(''); - obj.addToXML(''); - end - - % Convenience function to call writelines. - function addToXML(obj, line) - filename = fullfile(obj.helpDir, 'helptoc.xml'); - writelines(line, filename, 'WriteMode','append'); - end - - % Convenience function to generate HTML link to file. - % - % htmlFilename should include the .html file extension - function link = generateLink(~, htmlFilename, methodName) - link = ['' methodName '']; - end - - end - -end diff --git a/+kwave/+utilities/Utility_Functions.m b/+kwave/+utilities/Utility_Functions.m new file mode 100644 index 00000000..9d84546c --- /dev/null +++ b/+kwave/+utilities/Utility_Functions.m @@ -0,0 +1,24 @@ +%% Utility Functions +% * |formatDuration| +% * |getkWavePath| +% * |mustBeAllOrNoneEmpty| +% * |mustBeEmptyOrComplex| +% * |mustBeEqualSize| +% * |plotFieldsDiff| +% * |scaleSI| + +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . diff --git a/+kwave/+utilities/formatDuration.m b/+kwave/+utilities/formatDuration.m index ff44dc3f..c95f4131 100644 --- a/+kwave/+utilities/formatDuration.m +++ b/+kwave/+utilities/formatDuration.m @@ -1,4 +1,4 @@ -%% formatDuration +%% Format Duration % Package: kwave.utilities % % Format calendarDuration object into string containing hours, minutes, and @@ -8,8 +8,8 @@ % formattedDuration = formatDuration(elapsedTime); % %% Description -% The formatDuration function takes a or -% object as input and +% The formatDuration function takes a or +% object as input and % outputs a formatted string representing the duration. The output string % is composed of hours (if non-zero), minutes (if non-zero), and seconds, % each followed by the respective unit label (h, m, s). This function is @@ -33,11 +33,27 @@ % (h, m, s). % %% See Also -% * -% * -% * -% * -% * +% * +% * +% * +% * +% * + +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . function formattedDuration = formatDuration(elapsedTime) @@ -52,7 +68,7 @@ end [H, M, S] = hms(elapsedTime); - + formattedDuration = [num2str(S) 's']; if (M ~= 0) formattedDuration = [num2str(M) 'm ' formattedDuration]; @@ -60,5 +76,5 @@ if (H ~= 0) formattedDuration = [num2str(H) 'h ' formattedDuration]; end - + end diff --git a/+kwave/+utilities/getkWavePath.m b/+kwave/+utilities/getkWavePath.m index 241fc0b4..6aab7470 100644 --- a/+kwave/+utilities/getkWavePath.m +++ b/+kwave/+utilities/getkWavePath.m @@ -1,4 +1,4 @@ -%% getkWavePath +%% Get kWave Path % *Package:* kwave.utilities % % Return the path to the root k-Wave folder. @@ -12,6 +12,22 @@ %% Output Arguments % * |path| - (string) Path to k-Wave. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + function path = getkWavePath() path = fileparts(fileparts(fileparts(mfilename('fullpath')))); diff --git a/+kwave/+utilities/mustBeAllOrNoneEmpty.m b/+kwave/+utilities/mustBeAllOrNoneEmpty.m index 5bfe2ed7..78218e3c 100644 --- a/+kwave/+utilities/mustBeAllOrNoneEmpty.m +++ b/+kwave/+utilities/mustBeAllOrNoneEmpty.m @@ -1,4 +1,4 @@ -%% mustBeAllOrNoneEmpty +%% Must Be All Or None Empty % *Package:* kwave.utilities % % Validate that multiple inputs are either all empty, or none of them are @@ -22,12 +22,12 @@ % GNU Lesser General Public License as published by the Free Software % Foundation, either version 3 of the License, or (at your option) any % later version. -% +% % k-Wave-II is distributed in the hope that it will be useful, but WITHOUT % ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or % FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public % License for more details. -% +% % You should have received a copy of the GNU Lesser General Public License % along with k-Wave-II. If not, see . @@ -35,10 +35,10 @@ function mustBeAllOrNoneEmpty(varargin) % Check if any input is empty. anyEmptyInput = any(cellfun(@isempty, varargin)); - + % Check if all inputs are empty. allEmptyInputs = all(cellfun(@isempty, varargin)); - + % Throw an error if some inputs are empty while others are non-empty. if anyEmptyInput && ~allEmptyInputs eid = 'Validators:mustBeAllOrNoneEmpty'; diff --git a/+kwave/+utilities/mustBeEmptyOrComplex.m b/+kwave/+utilities/mustBeEmptyOrComplex.m index 57a3d193..861a9a58 100644 --- a/+kwave/+utilities/mustBeEmptyOrComplex.m +++ b/+kwave/+utilities/mustBeEmptyOrComplex.m @@ -1,4 +1,4 @@ -%% mustBeEmptyOrComplex +%% Must Be Empty Or Complex % *Package:* kwave.utilities % % Validate that an input is an empty array or a complex array / scalar. @@ -8,11 +8,27 @@ % %% Description % |mustBeEmptyOrComplex| throws an error if |a| is not empty or complex. -% This function does not return a value. +% This function does not return a value. % %% Input Arguments % * |a| - Input to validate. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + function mustBeEmptyOrComplex(a) if ~isempty(a) && isreal(a) eid = 'Type:notComplex'; diff --git a/+kwave/+utilities/mustBeEqualSize.m b/+kwave/+utilities/mustBeEqualSize.m index 555fcc20..86f54090 100644 --- a/+kwave/+utilities/mustBeEqualSize.m +++ b/+kwave/+utilities/mustBeEqualSize.m @@ -1,4 +1,4 @@ -%% mustBeEqualSize +%% Must Be Equal Size % *Package:* kwave.utilities % % Validate that two inputs are the same size. @@ -14,6 +14,22 @@ %% Input Arguments % * |a|, |b| - Inputs to validate. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + function mustBeEqualSize(a, b) if ~isequal(size(a), size(b)) eid = 'Size:notEqual'; diff --git a/+kwave/+utilities/parseTitle.m b/+kwave/+utilities/parseTitle.m deleted file mode 100644 index 1eaa8050..00000000 --- a/+kwave/+utilities/parseTitle.m +++ /dev/null @@ -1,29 +0,0 @@ -%% parseTitle -% *Package:* kwave.utilities -% -% Convenience function to extract the text appearing on the first line of -% an m-file after the characters "%% ". -% -%% Syntax -% titleParsed = parseTitle(filename) -% -%% Description -% |parseTitle| extracts the text appearing on the first line of an m-file -% after the characters "%% ". -% -%% Input Arguments -% * |filename| - (string) Filename to get title from. -% -%% Output Arguments -% * |titleString| - (string) Extracted title. - -function titleString = parseTitle(filename) - fid = fopen(filename); - titleLine = fgetl(fid); - if any(strfind(titleLine, "%% ")) - titleString = erase(titleLine, "%% "); - else - error('GenerateDocumentation:missingTitleComment', '%s is missing a title comment.', filename); - end - fclose(fid); -end diff --git a/+kwave/+utilities/plotFieldsDiff.m b/+kwave/+utilities/plotFieldsDiff.m index 3246ee3f..ab1ec509 100644 --- a/+kwave/+utilities/plotFieldsDiff.m +++ b/+kwave/+utilities/plotFieldsDiff.m @@ -1,4 +1,4 @@ -%% plotFieldsDiff +%% Plot Fields Diff % *Package:* kwave.utilities % % Plots two fields and the difference between them. @@ -20,6 +20,22 @@ %% Output Arguments % * |f| - (matlab.ui.Figure) Figure handle for generated figure. +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + function f = plotFieldsDiff(actual, expected, plotTitle) arguments diff --git a/+kwave/+utilities/scaleSI.m b/+kwave/+utilities/scaleSI.m index 98b2da19..95ec91e6 100644 --- a/+kwave/+utilities/scaleSI.m +++ b/+kwave/+utilities/scaleSI.m @@ -1,4 +1,4 @@ -%% scaleSI +%% Scale SI % *Package:* kwave.utilities % % Scale a number to the nearest SI unit prefix. @@ -44,6 +44,22 @@ %% See Also % * |kwave.utilities.formatDuration| +% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . + function [xScaled, scale, prefix, prefixFullName] = scaleSI(x) % Force the input to be a scalar. @@ -58,19 +74,19 @@ end if x == 0 - + % If x is zero, don't scale. xScaled = x; prefix = ''; prefixFullName = ''; scale = 1; - + elseif x < 1 - + % Update index and input. xScaled = x * 1e3; symbolIndex = 1; - + % Find scaling parameter. while xScaled < 1 && symbolIndex < 8 xScaled = xScaled * 1e3; @@ -111,20 +127,20 @@ prefix = 'y'; prefixFullName = 'yocto'; scale = 1e24; - end - + end + elseif x >= 1000 - + % Update index and input. xScaled = x * 1e-3; symbolIndex = 1; - + % Find scaling parameter. while xScaled >= 1000 && symbolIndex < 8 xScaled = xScaled * 1e-3; symbolIndex = symbolIndex + 1; end - + % Define SI unit scalings. switch symbolIndex case 1 @@ -160,15 +176,15 @@ prefixFullName = 'yotta'; scale = 1e-24; end - + else - + % If x is between 1 and 1000, don't scale. xScaled = x; prefix = ''; prefixFullName = ''; scale = 1; - + end % Form scaling into a string. diff --git a/.all-contributorsrc b/.all-contributorsrc new file mode 100644 index 00000000..81b6ff19 --- /dev/null +++ b/.all-contributorsrc @@ -0,0 +1,150 @@ +{ + "projectName": "k-wave-ii", + "projectOwner": "ucl-bug", + "repoType": "github", + "repoHost": "https://github.com", + "files": [ + "docs/README.md" + ], + "imageSize": 80, + "commit": false, + "commitConvention": "angular", + "contributors": [ + { + "login": "btreeby", + "name": "Bradley Treeby", + "avatar_url": "https://avatars.githubusercontent.com/u/4980942?v=4", + "profile": "https://github.com/btreeby", + "contributions": [ + "custom" + ] + }, + { + "login": "bencox", + "name": "Ben Cox", + "avatar_url": "https://avatars.githubusercontent.com/u/5031946?v=4", + "profile": "https://github.com/bencox", + "contributions": [ + "custom" + ] + }, + { + "login": "jarosjir", + "name": "Jiri Jaros", + "avatar_url": "https://avatars.githubusercontent.com/u/4980903?v=4", + "profile": "http://www.fit.vutbr.cz/~jarosjir/", + "contributions": [ + "custom" + ] + }, + { + "login": "ellymartin", + "name": "ellymartin", + "avatar_url": "https://avatars.githubusercontent.com/u/62395290?v=4", + "profile": "https://github.com/ellymartin", + "contributions": [ + "custom" + ] + }, + { + "login": "MatthewJohnKing", + "name": "MatthewJohnKing", + "avatar_url": "https://avatars.githubusercontent.com/u/137497118?v=4", + "profile": "https://github.com/MatthewJohnKing", + "contributions": [ + "custom" + ] + }, + { + "login": "astanziola", + "name": "Antonio Stanziola", + "avatar_url": "https://avatars.githubusercontent.com/u/3237202?v=4", + "profile": "https://github.com/astanziola", + "contributions": [ + "custom" + ] + }, + { + "login": "ellwise", + "name": "ellwise", + "avatar_url": "https://avatars.githubusercontent.com/u/45689261?v=4", + "profile": "https://github.com/ellwise", + "contributions": [ + "custom" + ] + }, + { + "login": "Devaraj-G", + "name": "Devaraj Gopinathan", + "avatar_url": "https://avatars.githubusercontent.com/u/36169767?v=4", + "profile": "https://profiles.ucl.ac.uk/61652", + "contributions": [ + "custom" + ] + }, + { + "login": "ilectra", + "name": "Ilektra Christidi", + "avatar_url": "https://avatars.githubusercontent.com/u/22891967?v=4", + "profile": "https://github.com/ilectra", + "contributions": [ + "custom" + ] + }, + { + "login": "dstansby", + "name": "David Stansby", + "avatar_url": "https://avatars.githubusercontent.com/u/6197628?v=4", + "profile": "https://www.davidstansby.com/", + "contributions": [ + "custom" + ] + }, + { + "login": "stellaprins", + "name": "Stella Prins", + "avatar_url": "https://avatars.githubusercontent.com/u/30465823?v=4", + "profile": "https://github.com/stellaprins", + "contributions": [ + "custom" + ] + }, + { + "login": "arindamsaha1507", + "name": "Arindam Saha", + "avatar_url": "https://avatars.githubusercontent.com/u/25665512?v=4", + "profile": "https://github.com/arindamsaha1507", + "contributions": [ + "custom" + ] + }, + { + "login": "qiUip", + "name": "Mashy Green", + "avatar_url": "https://avatars.githubusercontent.com/u/7360775?v=4", + "profile": "https://qiuip.github.io/", + "contributions": [ + "custom" + ] + }, + { + "login": "nicolin", + "name": "nicolin", + "avatar_url": "https://avatars.githubusercontent.com/u/6137757?v=4", + "profile": "https://github.com/nicolin", + "contributions": [ + "custom" + ] + } + ], + "contributorsPerLine": 6, + "linkToUsage": false, + "types": { + "custom": { + "symbol": "", + "description": "", + "link": "" + } + }, + "commitType": "docs" +} diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 00000000..9da8ba27 --- /dev/null +++ b/.codespellrc @@ -0,0 +1,2 @@ +[codespell] +ignore-words-list = nax,nd,ans,siz diff --git a/.github/ISSUE_TEMPLATE/bug-form.yml b/.github/ISSUE_TEMPLATE/bug-form.yml new file mode 100644 index 00000000..a9db62a4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-form.yml @@ -0,0 +1,58 @@ +name: Bug Report +description: File a bug report +title: "Bug: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report. + - type: textarea + id: description + attributes: + label: What Happened? + description: What happened and what did you expect to happen? + placeholder: What happened. + validations: + required: true + - type: textarea + id: logs + attributes: + label: Error messages + description: Please copy and paste any relevant error messages or output. This will be automatically formatted into code, so no need for backticks. + render: shell + - type: textarea + id: reproduce + attributes: + label: How Can We Reproduce? + description: Give detailed step-by-step instructions (including examples and/or code snippets) how the bug or error can be reproduced. We encourage you to try, as much as possible, to reduce your problem to the minimal example that still reproduces the issue. + placeholder: How to reproduce. + validations: + required: true + - type: dropdown + id: operating-system + attributes: + label: Operating System + description: What operating system are you using? + options: + - Windows + - Linux + - Mac + validations: + required: true + - type: textarea + id: kwave-version + attributes: + label: Release Version + description: What version of k-Wave-II are you using? + placeholder: k-Wave-II version + validations: + required: true + - type: textarea + id: matlab-version + attributes: + label: MATLAB Version + description: What version of MATLAB are you using? + placeholder: Matlab version. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/bug_template.md b/.github/ISSUE_TEMPLATE/bug_template.md deleted file mode 100644 index ff6e7234..00000000 --- a/.github/ISSUE_TEMPLATE/bug_template.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -name: Bug report -about: Report something not working -title: '' -labels: '' -assignees: '' ---- - -## Bug details - -### Description - -Description of the bug or error. - -### How to reproduce the error - -Detailed instructions (including code snippets) on how to reproduce the error. - -### Error message(s) - -Details of any error messages. - -### Error investigation / fix - -Details of any investigation into the root cause, and possible fixes. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..0086358d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: true diff --git a/.github/ISSUE_TEMPLATE/feature_request-form.yml b/.github/ISSUE_TEMPLATE/feature_request-form.yml new file mode 100644 index 00000000..aefe7601 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request-form.yml @@ -0,0 +1,37 @@ +name: Feature Request +description: Suggest a new feature or improvement +title: "Feature: " +labels: ["feature"] +body: + - type: textarea + id: problem-description + attributes: + label: Is your feature request related to a problem? Please describe. + description: A clear and concise description of what the problem is. + placeholder: Describe the problem your feature request addresses. + validations: + required: true + - type: textarea + id: solution-description + attributes: + label: Describe the solution you'd like + description: A clear and concise description of what you want to happen. + placeholder: Describe your proposed solution. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered. + placeholder: List any alternative solutions or features. + validations: + required: false + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Add any other context or screenshots about the feature request here. + placeholder: Any additional information or screenshots. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/issue_template.md b/.github/ISSUE_TEMPLATE/issue_template.md deleted file mode 100644 index 20b0503d..00000000 --- a/.github/ISSUE_TEMPLATE/issue_template.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: '' -assignees: '' ---- - -## Description - -Description of the feature or issue, and the code that might be required to implement it. - -Include details of the background, and possible approaches and algorithms. diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md deleted file mode 100644 index 33bc75e6..00000000 --- a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md +++ /dev/null @@ -1,23 +0,0 @@ -## Summary - -- Description of the code being merged, including any relevant implementation details. - -## Testing - -- Details of how the code was tested. - -## Related Issues - -* Closes #issueNum -* See also #issueNum - -## Peer Review - -Review summary (complete before merge): - -* [ ] Implementation is sensible -* [ ] All code is documented -* [ ] Code meets coding standard -* [ ] New unit test/s implemented and cover scope (if applicable) -* [ ] All unit test/s pass -* [ ] Issue description is complete diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..0ccb5658 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,31 @@ +## Summary + +- Description of the code being merged, including any relevant implementation details. + +## Testing + +- Details of how the code was tested. + +## Related Issues + +* Closes #issueNum +* See also #issueNum + +## Contributor Agreement + +* [ ] By submitting this pull request, I confirm I have read the Legal and Licensing section in `CONTRIBUTING.md`, and I agree that my contributions may be redistributed under the k-Wave-II open-source license. + +## Peer Review + +Review summary (complete before merge): + +- [ ] New (unit) test/s implemented +- [ ] All (unit) test/s pass +- [ ] New code is documented +- [ ] Documentation builds and looks right +- [ ] Examples/Tutorials added (if applicable) +- [ ] Linting tests pass +- Code review: + - [ ] Implementation is sensible + - [ ] Code meets coding standard + - [ ] Issue description is complete diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml index 6930256b..c5ddb055 100644 --- a/.github/workflows/code_quality.yml +++ b/.github/workflows/code_quality.yml @@ -1,14 +1,16 @@ name: Checks code quality on: pull_request: + types: [opened, synchronize, reopened, ready_for_review] push: branches: - main jobs: matlab-tests: - name: Run MATLAB Code Quality check - runs-on: Windows + name: Run MATLAB Code Quality check (skipped on draft PRs) + runs-on: custom-windows-with-matlab + if: github.event_name == 'push' || github.event.pull_request.draft == false steps: - name: Check out repository uses: actions/checkout@v3 diff --git a/.github/workflows/contributors.yml b/.github/workflows/contributors.yml new file mode 100644 index 00000000..47caf10f --- /dev/null +++ b/.github/workflows/contributors.yml @@ -0,0 +1,89 @@ +name: Update Contributors + +on: + pull_request: + +jobs: + all-contributors: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + fetch-depth: 0 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: | + npm install --no-save all-contributors-cli + + - name: Collect unique PR authors + co-authors (GitHub usernames) + id: authors + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + PR_NUMBER="${{ github.event.pull_request.number }}" + + gh api "repos/${{ github.repository }}/pulls/${PR_NUMBER}/commits" --paginate > pr_commits.json + + # 1) Commit authors (GitHub metadata) + AUTHOR_LOGINS=$(jq -r '.[].author.login // empty' pr_commits.json) + + # 2) Co-authors (parse commit message trailers) + COAUTHOR_LOGINS=$( + jq -r '.[].commit.message // ""' pr_commits.json \ + | awk 'BEGIN{IGNORECASE=1} /^co-authored-by:/{print}' \ + | sed -nE 's/.*<([^>]+)>.*/\1/p' \ + | sed -E 's/^([0-9]+\+)?([^@]+)@users\.noreply\.github\.com$/\2/I' \ + | grep -v '@' || true + ) + + ALL=$(printf "%s\n%s\n" "$AUTHOR_LOGINS" "$COAUTHOR_LOGINS" \ + | sed '/^$/d' \ + | grep -viE '\[bot\]$' \ + | sort -u) + + echo "GitHub users found (authors + resolvable co-authors):" + echo "$ALL" + + { + echo "list<> "$GITHUB_OUTPUT" + + - name: Add all commit authors as contributors + run: | + set -e + while IFS= read -r u; do + [ -z "$u" ] && continue + echo "Adding contributor: $u" + npx all-contributors add "$u" custom + done <<< "${{ steps.authors.outputs.list }}" + + npx all-contributors generate + + - name: Commit and push changes (if .all-contributorsrc changed) + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add .all-contributorsrc docs/README.md + + if git diff --cached --quiet -- .all-contributorsrc; then + echo "No changes in .all-contributorsrc." + exit 0 + fi + + echo "Changes detected — committing." + git commit -m "update contributors" + git push origin HEAD:${{ github.event.pull_request.head.ref }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 821c2166..1e74e938 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,18 +1,88 @@ name: Build docs on: pull_request: + types: [opened, synchronize, reopened, ready_for_review] push: branches: - - main + - develop + +permissions: + contents: read + pages: write + id-token: write jobs: - docs: - name: Build docs - runs-on: Windows + matlab-docs: + name: Build MATLAB docs (skipped on draft PRs) + runs-on: custom-windows-with-matlab # TODO: once repository is public, use gh hosted runner (issue #161) + if: github.event_name == 'push' || github.event.pull_request.draft == false steps: - name: Check out repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build docs run: | - matlab -batch "kwave.utilities.GenerateDocumentation" + & "C:\Program Files\MATLAB\R2025b\bin\matlab.exe" "-batch" "kwave.devtools.GenerateDocumentation" + + - name: Test docs + shell: bash + run: | + source docs/test_doc_generation.sh + + - name: Upload helpfiles + # uploading the generated helpfiles so that they are available for next job + uses: actions/upload-artifact@v6 + with: + name: docs-matlab + path: docs + + docs: + needs: matlab-docs + name: Build Mkdocs docs (skipped on draft PRs) + # skipped on draft PRs because the 'needs' condition (matlab-docs) will not be met + runs-on: windows-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Download helpfiles artifact + uses: actions/download-artifact@v4 + with: + name: docs-matlab + path: docs-matlab + + - name: Rename docs-matlab to docs + run: | + mv docs-matlab/helpfiles docs + mv docs-matlab/helpfilesweb docs + + - name: Build mkdocs + run: | + mkdocs build + + - uses: actions/upload-pages-artifact@v4 + with: + path: site + + deploy: + needs: docs + name: Deploy docs to GitHub Pages (skipped on draft PRs) + if: github.event_name == 'push' + runs-on: ubuntu-latest + 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/lint-license.yml b/.github/workflows/lint-license.yml new file mode 100644 index 00000000..21c6c354 --- /dev/null +++ b/.github/workflows/lint-license.yml @@ -0,0 +1,36 @@ +# Will check all .m files for the copyright statement and if not present will inject it +# and commit the changes +name: License HeaderFixer + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + paths: + - "**/*.m" + push: + +jobs: + apply-license: + runs-on: ubuntu-latest + permissions: + contents: write # Required to allow the bot to commit changes + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} # Checkout the PR branch specifically + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Run License Fixer Script + run: | + # Run the script + python docs/apply_license_mlab_check.py + + - name: Commit and Push Changes + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "style: automatically apply LGPL license headers to .m files" + file_pattern: "**/*.m" diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 00000000..23f9ba0d --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,18 @@ +name: pre-commit + +on: + pull_request: + push: + branches: + - main + - develop + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.x" + - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2837dfc7..4ee9a9fd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,14 +1,16 @@ name: Run MATLAB tests on: pull_request: + types: [opened, synchronize, reopened, ready_for_review] push: branches: - - main + - develop jobs: matlab-tests: - name: Run unit tests - runs-on: Windows + name: Run unit tests (skipped on draft PRs) + runs-on: custom-windows-with-matlab + if: github.event_name == 'push' || github.event.pull_request.draft == false steps: - name: Check out repository uses: actions/checkout@v3 diff --git a/.gitignore b/.gitignore index 0935247d..d5dd1e3c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,9 @@ ########### +kwave/+tests/unit-test-artifacts -helpfiles/* +docs/helpfiles/* +docs/helpfilesweb/* +node_modules/ ###################### # OS generated files # @@ -28,3 +30,21 @@ Thumbs.db # Test + doc autogenerated files # ################################## +kwave/+tests/*artifacts + + +################### +# VSCode settings # +################### + +.vscode/ + +################# +# GitHub Pages # +################# + +site/ + +############### +# Python env # +############### +.venv/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..7b2f064a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,31 @@ +exclude: ^(\+kwave/\+legacy/|\+kwave/site/) + +repos: + - repo: https://github.com/rbubley/mirrors-prettier + rev: v3.8.1 + hooks: + - id: prettier + exclude: ^\+kwave/docs + exclude_types: [markdown] + args: + - --quote-props=as-needed + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-added-large-files + - id: check-case-conflict + - id: check-merge-conflict + - id: detect-private-key + - id: end-of-file-fixer + - id: mixed-line-ending + args: + - --fix=lf + - id: no-commit-to-branch + args: + - --branch=main + - --branch=develop + - id: trailing-whitespace + - repo: https://github.com/codespell-project/codespell + rev: v2.4.1 + hooks: + - id: codespell diff --git a/README.md b/README.md deleted file mode 100644 index 3d4c503a..00000000 --- a/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# k-Wave-II: A MATLAB toolbox for the simulation of acoustic wave fields - -[![License: LGPL v3](https://img.shields.io/badge/License-LGPL_v3-blue.svg)](LICENSE.txt) [![codecov](https://codecov.io/gh/ucl-bug/k-wave-ii/graph/badge.svg?token=A8UMTDRF6T)](https://codecov.io/gh/ucl-bug/k-wave-ii) ![unit tests](https://github.com/ucl-bug/k-wave-ii/actions/workflows/tests.yml/badge.svg) ![code quality](https://github.com/ucl-bug/k-wave-ii/actions/workflows/code_quality.yml/badge.svg) ![build docs](https://github.com/ucl-bug/k-wave-ii/actions/workflows/docs.yml/badge.svg) - -## Overview - -### Project goals - -k-Wave-II is a major re-write of the original k-Wave Toolbox, developed -with the following aims: - -1. Re-engineering the code base to leverage object orientated programming -and differentiable functions for deep learning and coupled physics problems -2. Extending the algorithms to facilitate general boundary conditions on -arbitrary surfaces, and to increase performance for narrow-band simulations -3. Improving the development and release process to incorporate good -practice and advance long-term sustainability -4. Improving training, user engagement, and support. - -## Getting started - -### Minimum requirements - -- k-Wave-II requires MATLAB 2022b or later. -- No MATLAB toolboxes are required to use k-Wave-II. However, running the unit tests requires the signal processing toolbox for the reference `sinc` function. -- If importing the k-Wave-II namespace (using `import kwave.toolbox.*`) it is recommended that k-Wave-I is NOT on the MATLAB path to avoid naming conflicts. - -### Building the documentation - -- All documentation (including developer documentation) is written in `.m` files and automatically compiled to `.html` viewable in the MATLAB help browser. -- The documentation can be compiled by calling `kwave.utilities.GenerateDocumentation` in the root folder. -- After compiling, the documentation can be viewed by opening the MATLAB help browser and selecting **k-Wave II** from the list of supplemental software. -- New developers should read the [`Developer Introduction`](+kwave/+docfiles/+developer/developerIntroduction.m). - -### Running the tests - -- Tests are written using the MATLAB unit testing framework. To run the tests locally, call: - - `kwave.tests.runTests(TestType=kwave.tests.TestType.unit)` - - `kwave.tests.runTests(TestType=kwave.tests.TestType.linting)` diff --git a/docs/CITATION.cff b/docs/CITATION.cff new file mode 100644 index 00000000..5ae44c02 --- /dev/null +++ b/docs/CITATION.cff @@ -0,0 +1,48 @@ +# This CITATION.cff file was generated with cffinit. +# Visit https://bit.ly/cffinit to generate yours today! + +cff-version: 1.2.0 +title: k-Wave-II +message: >- + If you use this software, please cite it using the + metadata from this file. +type: software +authors: + - given-names: Bradley + family-names: Treeby + - given-names: Ben + family-names: Cox +identifiers: + - type: doi + value: 10.1121/1.4712021 +repository-code: "https://github.com/ucl-bug/k-wave-ii" +url: "https://ucl-bug.github.io/k-wave-ii/" +abstract: >- + k-Wave-II is an open-source MATLAB toolbox used to solve + partial differential equations, with a particular focus on + wave problems in biomedical ultrasound. +keywords: + - ultrasound + - wave propagation + - nonlinear acoustics + - acoustic simulation + - photoacoustics +license: LGPL-3.0 +preferred-citation: + type: article + authors: + - family-names: Treeby + given-names: B. E. + - family-names: Jaros + given-names: J. + - family-names: Rendell + given-names: A. P. + - family-names: Cox + given-names: B. T. + title: "Modeling nonlinear ultrasound propagation in heterogeneous media with power law absorption using a k-space pseudospectral method" + journal: The Journal of the Acoustical Society of America + volume: "131" + issue: "6" + pages: "4324–4336" + year: 2012 + doi: 10.1121/1.4712021 diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 00000000..66f64f46 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,107 @@ +# Contributor guidelines + +## Development Workflow + +The k\-Wave-II development workflow follows [git-flow](https://nvie.com/posts/a-successful-git-branching-model/), modernised for an open-source project to [use forks](https://docs.github.com/en/get-started/exploring-projects-on-github/contributing-to-a-project) instead of branches: + +- The `main` branch is used only for releases. Any code development branches off and eventually merges back into the `develop` branch, via feature branches. For more details on this workflow, see the [maintainer guidelines](maintainerDocs.md) +- Start by [creating a fork](https://docs.github.com/en/get-started/exploring-projects-on-github/contributing-to-a-project#creating-your-own-copy-of-a-project) of the GitHub k-Wave-II repo. Make sure you copy all branches, not only the default. Now you have your own copy of the whole repo on GitHub. + You can manage that fork as you please, but it is good practice to keep its `main`, `develop`, and feature branches in synch with the upstream k-Wave-II repo, and do your development on different branches. +- Clone the fork locally to work on your code changes +``` +git clone https://github.com/YOUR-USER-NAME/k-wave-ii +``` +- Any code development you would like to contribute to k-Wave-II, has to relate to an already reported issue in the upstream repo. For existing issues under active development, an appropriate feature branch would already exist. Those are descriptively named, starting with the issue number, e.g., `62-implement-pml-class`. If an appropriate feature branch does not exist, [open an issue](../#getting-help) in the upstream repo to request one. You will then need to re-synch your fork and `git pull` to get that branch locally. +- Create a branch off the feature branch you want to contribute to, and switch to it, e.g. +``` +git checkout 62-implement-pml-class +git switch -c 75-implement-pml-class-getters +``` +Note that, in this case, your development refers to a different issue (`75` in this fictional example), that should ideally be a [sub-issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/adding-sub-issues) of the larger feature issue (`62` in this example). +If you want to work on the parent issue directly, use its number instead, together with something that identifies you in the name of the branch, e.g. +``` +git switch -c 62-implement-pml-class-YOUR_USER_NAME +``` + +- [Set up a development environment](#setting-up-a-development-environment). This is not needed for the matlab code to run, but for the various tools you will need for development. +- **Now that you are ready to code, please read the [developers intro](developerIntroduction.md) and [coding standard](codingStandard.md)!** +- Commit to that branch locally and regularly push your work to the same named branch on the fork. +- Label commit messages with the issue number, e.g., `git commit -m "#62: Basic class structure"` +- If you need feedback or help but your branch is not ready to merge, open a draft pull request (PR) from your branch in your fork, to the feature branch in the upstream repo. Likewise, when you think the branch is ready for merging, open a (normal) PR, or convert your draft one to a normal PR, and request a code review. + +For experienced git users, `git rebase` should be avoided if multiple people might be contributing to a branch (use `git merge` instead). + +## Setting up a Development Environment + +**1. Create and activate an environment** +Use an environment manager to create and activate a python environment. + +For example, with [conda](https://docs.conda.io/projects/conda/en/latest/index.html): + +```bash +conda create -n kwave +conda activate kwave +``` + +**2. Install dependencies** +Install packages defined in `requirements.txt`: + +``` +pip install -r requirements.txt +``` + +This installs [`mkdocs`](https://www.mkdocs.org/) used to build the documentation (see [Writing and Building the Documentation](developerIntroduction.md#writing-and-building-the-documentation)). + +If you want to skip the pre-commit checks when you commit a change, you can use `git commit --no-verify`. However, the same tests will be run automatically on the CI when you push your changes to GitHub, and they will fail at that point if they identify any required fixes. We strongly suggest you fix any issues identified by pre-commit locally, before committing and pushing to the repository. + +**3. Install pre-commit** +We use [pre-commit](https://pre-commit.com/) which runs automated checks (formatting, spelling, line endings, etc.) on every `git commit` to keep the codebase consistent. + +Install and enable it with: + +```bash +pre-commit install +``` + +To run pre-commit manually: + +```bash +pre-commit run +``` + +By default, the pre-commit checks will be run only on the files that changed. If you want to run it on all files, use `pre-commit run -a` instead. + +## AI and LLM Use + +AI tools can be valuable for learning, exploring ideas, and accelerating work. We welcome their thoughtful use. To ensure contributions remain meaningful and contributors continue to grow, we ask that you: + +* Understand what you submit. Be able to explain and defend any contribution you make. If you cannot, it is not ready to submit. +* Stay in the driver's seat. Use AI to support your learning and work, not to replace the effort and critical thinking that make you a better developer. +* Engage meaningfully. Low-effort, AI-generated submissions (issues, PRs, or proposals) without genuine personal engagement are not acceptable. + +Maintainers may ask contributors to explain their work. This is part of our commitment to learning and quality—not a test of whether you used AI, but whether you understood and own what you contributed. + +## Conditions for merging a PR + +Only the k-Wave-II maintainers can merge PRs into any of the branches of the upstream repo. +The pull request template will guide you through the requirements to get your changes approved and merged by the maintainers. For reference, those are + +- If adding a new function or class, add appropriate tests. See the [developer docs](../developerIntroduction#testing-framework) for more details. +- All tests, existing or new, should pass. You should strive to run the tests locally before you open the PR, in order to catch errors early, but the Github Actions automation on the k-Wave-II repo will run them automatically as well when you open a PR. +- [Update the documentation](../developerIntroduction#writing-and-building-the-documentation) and make sure it builds and looks right. +- Add examples and/or tutorials if you added more substantial functionality. +- One approving code review by one of the maintainers. + +The code review step is a crucial one, to guarantee the quality of code contributions from the community. It is an iterative process, and you will have to address the reviewer's comments and any concerns. Keep in mind that those comments are given in good faith and not as judgement on anyone's coding ability, and are meant to support our community of developers in creating the best software we can, for all of us to use. Seasoned developers would testify to how much they have learned and improved in their work by receiving reviews on their codes. + + +## Legal and Licensing +To protect the interests of the k-Wave-II community and ensure the long-term sustainability of the project, we require all contributors to adhere to our [licensing terms](LICENSE.md). + +### Contributor Agreement +By contributing to k-Wave-II, you agree that we may redistribute your work under the project's current open-source license. You represent that you are legally entitled to grant this permission and that your contribution does not infringe on the intellectual property rights of others. + +For more information on why this is necessary and how ownership works in open source, please refer to the following resources from OSS Watch: + +* [Open source development - An introduction to ownership and licensing issues](http://oss-watch.ac.uk/resources/iprguide) +* [Contributor Licence Agreements (CLAs)](http://oss-watch.ac.uk/resources/cla) diff --git a/LICENSE.txt b/docs/LICENSE.md similarity index 97% rename from LICENSE.txt rename to docs/LICENSE.md index 80d241e1..72d2d56d 100644 --- a/LICENSE.txt +++ b/docs/LICENSE.md @@ -1,165 +1,167 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - - This version of the GNU Lesser General Public License incorporates -the terms and conditions of version 3 of the GNU General Public -License, supplemented by the additional permissions listed below. - - 0. Additional Definitions. - - As used herein, "this License" refers to version 3 of the GNU Lesser -General Public License, and the "GNU GPL" refers to version 3 of the GNU -General Public License. - - "The Library" refers to a covered work governed by this License, -other than an Application or a Combined Work as defined below. - - An "Application" is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. - - A "Combined Work" is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the "Linked -Version". - - The "Minimal Corresponding Source" for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. - - The "Corresponding Application Code" for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. - - 1. Exception to Section 3 of the GNU GPL. - - You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. - - 2. Conveying Modified Versions. - - If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: - - a) under this License, provided that you make a good faith effort to - ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or - - b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. - - 3. Object Code Incorporating Material from Library Header Files. - - The object code form of an Application may incorporate material from -a header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: - - a) Give prominent notice with each copy of the object code that the - Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the object code with a copy of the GNU GPL and this license - document. - - 4. Combined Works. - - You may convey a Combined Work under terms of your choice that, -taken together, effectively do not restrict modification of the -portions of the Library contained in the Combined Work and reverse -engineering for debugging such modifications, if you also do each of -the following: - - a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the Combined Work with a copy of the GNU GPL and this license - document. - - c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. - - d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of this - License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (a) uses at run time - a copy of the Library already present on the user's computer - system, and (b) will operate properly with a modified version - of the Library that is interface-compatible with the Linked - Version. - - e) Provide Installation Information, but only if you would otherwise - be required to provide such information under section 6 of the - GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the - Application with a modified version of the Linked Version. (If - you use option 4d0, the Installation Information must accompany - the Minimal Corresponding Source and Corresponding Application - Code. If you use option 4d1, you must provide the Installation - Information in the manner specified by section 6 of the GNU GPL - for conveying Corresponding Source.) - - 5. Combined Libraries. - - You may place library facilities that are a work based on the -Library side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: - - a) Accompany the combined library with a copy of the same work based - on the Library, uncombined with any other library facilities, - conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - - 6. Revised Versions of the GNU Lesser General Public License. - - The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - - Each version is given a distinguishing version number. If the -Library as you received it specifies that a certain numbered version -of the GNU Lesser General Public License "or any later version" -applies to it, you have the option of following the terms and -conditions either of that published version or of any later version -published by the Free Software Foundation. If the Library as you -received it does not specify a version number of the GNU Lesser -General Public License, you may choose any version of the GNU Lesser -General Public License ever published by the Free Software Foundation. - - If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library. \ No newline at end of file +```txt + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. +``` diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..5c6b26a8 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,97 @@ +# k-Wave-II: A MATLAB toolbox for the simulation of acoustic wave fields + +!!! warning "Under Construction" + K‑Wave ii is currently under active development and not public yet. + Documentation, APIs, and features may change without notice. + +[![License: LGPL v3](https://img.shields.io/badge/License-LGPL_v3-blue.svg)](LICENSE.md) [![codecov](https://codecov.io/gh/ucl-bug/k-wave-ii/graph/badge.svg?token=A8UMTDRF6T)](https://codecov.io/gh/ucl-bug/k-wave-ii) ![unit tests](https://github.com/ucl-bug/k-wave-ii/actions/workflows/tests.yml/badge.svg) ![code quality](https://github.com/ucl-bug/k-wave-ii/actions/workflows/code_quality.yml/badge.svg) ![build docs](https://github.com/ucl-bug/k-wave-ii/actions/workflows/docs.yml/badge.svg) +[![All Contributors](https://img.shields.io/badge/all_contributors-14-orange.svg?style=flat-square)](#contributors-) + + +k\-Wave-II is an open\-source MATLAB toolbox used to solve partial differential equations, with a particular focus on wave problems in biomedical ultrasound. The unifying thread for the solvers is that spatial gradients are computed using a Fourier collocation spectral method. This has many advantages, including spectral convergence for smooth functions, and a known analytical form for the bandlimited interpolant, which is useful for implementing staircase free sources, for example. + +k-Wave-II is a major re-write of the original [k-Wave](https://github.com/ucl-bug/k-wave) Toolbox, developed + +with the following aims: + +1. Re-engineering the code base to leverage object orientated programming +and differentiable functions for deep learning and coupled physics problems +2. Extending the algorithms to facilitate general boundary conditions on +arbitrary surfaces, and to increase performance for narrow-band simulations +3. Improving the development and release process to incorporate good +practice and advance long-term sustainability +4. Improving training, user engagement, and support. + +## Releases + +The current release is _XX.YY.ZZ (link to the Changelog/Release notes here)_. + +We aim to release a new version of the toolbox every May and November, with patches issued in-between as needed. If you want to use the latest features between releases, you can clone the [github repo](https://github.com/ucl-bug/k-wave-ii/tree/main) and use the `develop` branch. If you choose to do that, please keep in mind that those features might not be complete yet, and therefore we would appreciate your feedback and your patience. + +Every k-Wave-II release is tested with all releases of MATLAB after the minimum (see [Minimum requirements](#minimum-requirements)), until the latest one before the release was made. + +## Getting started + +### Minimum requirements + +- k-Wave-II requires MATLAB 2022b or later. +- No MATLAB toolboxes are required to use k-Wave-II. However, running the unit tests requires the signal processing toolbox for the reference `sinc` function. +- If importing the k-Wave-II namespace (using `import kwave.toolbox.*`) it is recommended that k-Wave-I is NOT on the MATLAB path to avoid naming conflicts. + +### Using k-Wave-II + +For examples of how to use k-Wave-II, have a look at the [tutorials](helpfilesweb/Tutorials/SUMMARY.md) and the [toolbox classes documentation](helpfilesweb/Toolbox_Functions/SUMMARY.md). + +## Getting help + +We are a very small team maintaining this toolbox, and we would like it to be community-driven and sustained as much as possible. + +If you would like to report a bug or request a new feature, first check the [existing issues](https://github.com/ucl-bug/k-wave-ii/issues) in the github repo and contribute to the discussion if it is already reported. If it is not, please open a new issue, using the appropriate template. And then, please consider contributing to the fix or improvement (see the section on [contributing](#contributing)). + +If you have a question or you need some help, please use the [discussions feature](https://github.com/ucl-bug/k-wave-ii/discussions) in the github repo. Hopefully someone from the community, either a maintainer, developer, or user will be able to assist you. + +## Contributing + +The user community is very welcome to contribute to the code and open pull requests for new features and bug fixes. New developers should read the developer docs first, starting with the [contributor guidelines](CONTRIBUTING.md). + +We also welcome further involvement by people wishing to review developers' code contributions and help the community of developers grow. Please contact the maintainers if you are interested in becoming a code reviewer. + +## Development lead +k‑Wave‑II builds on long‑standing collaborations and the contributions of many researchers, building on foundational work by Bradley Treeby, Ben Cox, and Jiri Jaros. Its ongoing development is guided and maintained by [the UCL Biomedical Ultrasound Group](http://bug.medphys.ucl.ac.uk/). + +## Contributors + + + + + + + + + + + + + + + + + + + + + + + + + + +
Bradley Treeby
Bradley Treeby

Ben Cox
Ben Cox

Jiri Jaros
Jiri Jaros

ellymartin
ellymartin

MatthewJohnKing
MatthewJohnKing

Antonio Stanziola
Antonio Stanziola

ellwise
ellwise

Devaraj Gopinathan
Devaraj Gopinathan

Ilektra Christidi
Ilektra Christidi

David Stansby
David Stansby

Stella Prins
Stella Prins

Arindam Saha
Arindam Saha

Mashy Green
Mashy Green

nicolin
nicolin

+ + + + + + + +The grid above is generated with [all-contributors](https://allcontributors.org/) using information from GitHub. If you’d like to request a change contact [the maintainers](mailto:b.cox@ucl.ac.uk). diff --git a/docs/apply_license_mlab_check.py b/docs/apply_license_mlab_check.py new file mode 100644 index 00000000..cd916af4 --- /dev/null +++ b/docs/apply_license_mlab_check.py @@ -0,0 +1,145 @@ +# This script enforces a license header for .m files by +# replacing "wrong" or outdated license blocks + +import os +import sys + +#============================================================================== +# Config: Set to True to see detailed line-by-line actions in the console +VERBOSE = True + +LICENSE_TEXT = """% Copyright (C) 2024- The k-Wave Authors. +% +% This file is part of k-Wave-II (http://www.k-wave.org). k-Wave-II is free +% software: you can redistribute it and/or modify it under the terms of the +% GNU Lesser General Public License as published by the Free Software +% Foundation, either version 3 of the License, or (at your option) any +% later version. +% +% k-Wave-II is distributed in the hope that it will be useful, but WITHOUT +% ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +% FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public +% License for more details. +% +% You should have received a copy of the GNU Lesser General Public License +% along with k-Wave-II. If not, see . +""" +#============================================================================== + +#============================================================================== +# 1] Unique strings to identify the boundaries of the license block +START_ID = "% Copyright (C)" +END_ID = "along with k-Wave-II. If not, see ." +#============================================================================== + +#============================================================================== +# 2] Core logic: If license is missing or wrong, it will replace/add the block +def process_file(path): + """Checks and fixes a single .m file. Follows Help-First order.""" + if not path.lower().endswith(".m"): + return + + try: + with open(path, 'r', encoding='utf-8') as f: + lines = f.readlines() + except Exception as e: + if VERBOSE: print(f" [!] Skip {path}: {e}") + return + + content = "".join(lines) + + # Check if the exact current license is already there + if LICENSE_TEXT in content: + if VERBOSE: print(f"Licence already there, skipping {path}") + return + + if VERBOSE: print(f"\n>>> Processing: {path}") + + # Detect and remove outdated/wrong blocks to enforce requirement 2] + start_idx = -1 + end_idx = -1 + for i, line in enumerate(lines): + if START_ID in line: + start_idx = i + if END_ID in line: + end_idx = i + break + + if start_idx != -1 and end_idx != -1: + if VERBOSE: print(f" [-] Removing outdated/wrong license (Lines {start_idx+1}-{end_idx+1})") + del lines[start_idx : end_idx + 1] + elif VERBOSE: + print(f" [+] No valid license found. Preparing fresh insertion.") + + # ------------------------------------------------------------------------- + # Help-First Insertion Logic: + # 1. Skip the function/classdef line (if it exists) + # 2. Skip the first contiguous block of comments (Help text) + # 3. Insert license there + # ------------------------------------------------------------------------- + insert_idx = 0 + in_help_block = False + + for i, line in enumerate(lines): + stripped = line.strip() + + # Skip function or classdef at the very top + if i == 0 and (stripped.startswith('function') or stripped.startswith('classdef')): + insert_idx = i + 1 + continue + + # If we hit a comment, we are in the help block + if stripped.startswith('%'): + in_help_block = True + insert_idx = i + 1 + else: + # If we hit an empty line or code AFTER having seen comments, + # the help block is over. + if in_help_block: + break + # If we hit code before seeing any comments, insert right here (no help text exists) + if stripped and not stripped.startswith('%'): + break + + if VERBOSE: print(f" [*] Inserting fresh license at line {insert_idx + 1}") + + # Assemble new content + new_lines = lines[:insert_idx] + + # Add spacing for readability + if insert_idx > 0 and not new_lines[-1].isspace(): + new_lines.append("\n") + + new_lines.append(LICENSE_TEXT) + + # Add spacing after if code follows + if insert_idx < len(lines) and not lines[insert_idx].isspace(): + new_lines.append("\n") + + new_lines.extend(lines[insert_idx:]) + + with open(path, 'w', encoding='utf-8') as f: + f.writelines(new_lines) +#============================================================================== + +#============================================================================== +# 3] Script Execution: Supports targeting a specific file or directory +if __name__ == "__main__": + if VERBOSE: print("Starting license enforcement check") + + target_path = sys.argv[1] if len(sys.argv) > 1 else os.getcwd() + + if os.path.isfile(target_path): + process_file(target_path) + else: + for root, _, files in os.walk(target_path): + if ((any(part.startswith('.') for part in root.split(os.sep))) or # skip dot files + ("+legacy" in root) or # skip +legacy folder + "scripts" in root): # skip scripts folder + if VERBOSE: print(f"Skipping {root}") + continue + for file in files: + process_file(os.path.join(root, file)) + + if VERBOSE: print("\nLicense enforcement check complete.") +#============================================================================== diff --git a/docs/codeOfConduct.md b/docs/codeOfConduct.md new file mode 100644 index 00000000..b4e8cde7 --- /dev/null +++ b/docs/codeOfConduct.md @@ -0,0 +1,52 @@ +# Code of Conduct + + +## Our Commitment + +We are committed to fostering a respectful, inclusive, and harassment-free community for everyone — regardless of age, disability, ethnicity, sex characteristics, gender identity or expression, experience level, education, socio-economic status, nationality, appearance, race, caste, color, religion, or sexual orientation. + +We aim to build an open, welcoming, diverse, and healthy environment for all participants. + +## Expected Behavior + +We expect community members to: + +- Show empathy and kindness +- Respect differing opinions and experiences +- Give and receive constructive feedback +- Take responsibility for mistakes and learn from them +- Prioritize the well-being of the community + +## Unacceptable Behavior + +The following will not be tolerated: + +- Sexualized language, imagery, or advances +- Harassment, trolling, insults, or personal/political attacks +- Publishing private information without permission +- Any conduct inappropriate for a professional setting + +## Enforcement + +Community leaders are responsible for enforcing this Code of Conduct. They may remove or reject contributions that violate these standards and will act fairly and appropriately in response to incidents. + +Reports of unacceptable behavior can be made to [the maintainers](mailto:b.cox@ucl.ac.uk) + +All reports will be handled promptly and confidentially. + +## Consequences + +Actions will be taken based on the severity and pattern of behavior: + +- Correction – Private warning and guidance. +- Warning – Formal warning with temporary interaction limits. +- Temporary Ban – Short-term removal from community spaces. +- Permanent Ban – Removal from the community. + +## Scope + +This Code applies in all community spaces and when representing the community publicly. + +Attribution + +Adapted from the [Contributor Covenant Code of Conduct](https://www.contributor-covenant.org/version/3/0/code_of_conduct/). diff --git a/docs/codingStandard.md b/docs/codingStandard.md new file mode 100644 index 00000000..7d144e99 --- /dev/null +++ b/docs/codingStandard.md @@ -0,0 +1,130 @@ +# Coding Standard + +MATLAB coding standard used for k\-Wave II. + +## Naming + +- Namespaces uses `+lowercase`. +- Class names use `TitleCase`. +- Function names and script filenames use `camelCase`. +- Test classes use `TestMyClassName` or `TestMyFunctionName`. +- Variables use `camelCase`. +- Name\-Value arguments use `TitleCase`. +- Descriptive naming should be used for the user\-facing API where possible, e.g., `soundSpeed` rather than `c`. Break this rule for internal variables for commonly used symbols (e.g. `c` for sound speed, `p` for pressure), as long as the symbol is well known and stays within a limited scope (e.g., a function) where a short comment at the top says what the single letter stands for. + +## Code + +- Each line of code should only contain one statement. +- Each line of executed code should be terminated with a semicolon without a preceding space. +- Class properties and argument validation code should not be terminated with a semicolon. +- For element\-wise multiply, divide, and power operations, use a preceding period, even when acting on a scalar. +- Always use a period before the transpose operator, unless the conjugate transpose is required. +- Mathematical operands and assignments, e.g., `+ * && = >` etc., should be space padded. Exceptions are power `^`, the colon operator `:`, the not operator `~`, and the `-` sign when used to signify a negative number. +```matlab +x = 1:10; +x = a .* b.^2; +x = a && ~b; +x = 10 - 3.4; +x = -1e-3; +x = [1, 2, 3].'; +``` + +- Break the previous two rules if it helps readability for simple multiply and divide operations using scalar literals. +```matlab +x = 2*pi; +x = 1/pi; +``` + +- Decimals should have a preceding zero. +- Always precede the imaginary unit with a number, e.g., `1i`. +```matlab +x = 0.5 + 1i; +``` + +- Align multiple similar lines of code if it makes sense for readability (similarly for comments). +```matlab +windSpeed = exp( x - w .* t); +waterTemperature = exp(-x - 3*w .* t); +``` + +- If long lines of code become difficult to read, then break lines of code using `...` and align for readability. + +## Brackets and Commas + +- Brackets should have no interior spaces. Break this rule if needed for readability when using multiple nested brackets. +- Brackets should follow immediately after function names with no space. +- Commas should be used to separate multiple inputs, outputs, or matrix values. +- All commas should be followed by a space. +```matlab +x = (a - b).^2; +[a, b, c] = myFunc(x, y, z); +x = [1, 2, 3]; +x = [1; 2; 3]; +``` + +- Logical statements should be grouped using brackets +```matlab +if (myHeight > 10) && (myDog.Name == "Fido") + disp('Excellent.'); +end +``` + +## Code Comments + +- All code comments should be written as sentences, including capitals and periods. +- Comments should be limited to a maximum column width of 75 characters (the default in MATLAB). +- Comments that span multiple lines should be aligned at the first character of each line. +- Code comments should appear on the previous line, rather than inline. Break this rule with common sense. +- Comments should be preceded by a blank line. + +## Indentation and Line Breaks + +- Statements inside conditional loops and if\-statements (etc) should be indented. +- Each code indent level should be 4 spaces. +- Include a blank line at the beginning and end of the conditional IFF the code on the inside contains a comment. +- All files should end with a blank new line. +- Trailing spaces after lines of code or comments should be removed. +```matlab +% Increment the counter if radius exceed maximum value. There are no +% comments inside the if statement, so do not include additional line +% breaks. +if radius > maximumRadius + counter = counter + 1; +end + +% Loop through possible heights and calculate weight. There are comments +% inside the if state, so include additional line breaks. +for index = 1:length(heightArray) + + % Assign current height. + height = heightArray(index); + + % Calculate current weight based on time of day. + weight(index) = height .* now; + +end +``` + +## Error messages + +- Error messages should use the `kwave.toolbox.Logger` class. +- Error messages should begin with a capital letter (except when using a variable or function name) and end with a period. +- Error messages should use a two part identifier in the form `ClassName:errorIdentifier` or `functionName:errorIdentifier`. +```matlab +kwave.toolbox.Logger.error('Grid:incorrectInputSize', 'gridSpacing must be a scalar or the same length as gridSize.'); +``` + +## Class and Function Documentation + +Classes should be written using [methods in separate files](https://uk.mathworks.com/help/matlab/matlab_oop/methods-in-separate-files.html). Class and function documentation should be written using [publishing markup](https://uk.mathworks.com/help/matlab/matlab_prog/marking-up-matlab-comments-for-publishing.html) following [the template](helpfilesweb/classDocsExample.md) (to open the template in the MATLAB Editor, `edit('kwave.docfiles.general.classDocsExample')`). The following structure and headings should be used + +- functionName or className +- Syntax +- Description +- Examples +- Input Arguments +- Name\-Value Arguments +- Output Arguments +- Properties (classes only) +- Methods (classes only) +- See Also diff --git a/docs/developerIntroduction.md b/docs/developerIntroduction.md new file mode 100644 index 00000000..b7529134 --- /dev/null +++ b/docs/developerIntroduction.md @@ -0,0 +1,113 @@ +# Developer Introduction + +Introduction to k\-Wave II for developers. + +## Overview + +k\-Wave II is an open\-source MATLAB toolbox used to solve differential equations, with a particular focus on wave problems in acoustics. The unifying thread for the solvers is that spatial gradients are computed using a Fourier collocation spectral method. This has many advantages, including spectral convergence for smooth functions, and a known analytical form for the band\-limited interpolant, which is useful for implementing stair\-case free sources, for example. + + +The main components of the toolbox are written using an object\-orientated programming design approach, primarily using `handle` classes. Sets of functionality (such as medium inputs for a particular solver) are grouped into classes. Base classes are used for common functionality that needs to be re\-used multiple times. This adheres to the don't repeat yourself (DRY) software development principle. Some additional helper functions with limited scope are written as standalone functions. + + +The software is written with the following four guiding principles, in decreasing order of importance: + +1. **The code is accurate.** The solvers should avoid narrow scope assumptions which are not always applicable. The code should also implement appropriate input validation and error handling, and be covered by appropriate tests (see [Testing Framework](#testing-framework) (testingFramework.md) for further details). +2. **The code is easy to use.** Careful thought should be given to the class interfaces and default values, and the user\-facing API should be as stable as possible over time. All code should be well documented, and the documentation should include user examples and tutorials. Code should also provide sufficient feedback and warnings to guide users. +3. **The code is sustainable.** The development, testing, and release workflows should be sufficiently automated that it is straightforward to implement new features and bug fixes. All code should follow the [Coding Standard](codingStandard.md). +4. **The code is fast.** Code should be profiled regularly, and where possible, refactored to improve computational efficiency. + +## Repository Structure + +The code is grouped into [package folders](https://uk.mathworks.com/help/matlab/matlab_oop/scoping-classes-with-packages.html) (namespaces). The basic folder structure is as follows: + +``` + ├── .github + │ ├── ISSUE_TEMPLATE (GitHub issue templates) + │ ├── PULL_REQUEST_TEMPLATE (GitHub pull request template) + │ └── workflows (GitHub actions) + ├── +kwave + │ ├── +devtools (Developer tools) + | ├── +docfiles (Documentation files) + │ │ └── +general (Additional Documentation Pages) + │ ├── +legacy (Copy of k-Wave I) + │ ├── +tests (Tests) + │ │ ├── +legacy (Regression tests against k-Wave I) + │ │ ├── +linting (Linting tests) + │ │ └── +unit (Unit tests) + │ ├── +toolbox (Main classes and functions) + │ ├── +tutorials (Examples and tutorials) + │ └── +utilities (Other tools) + └── docs (Contributor guidelines and other static docs) + ├── helpfiles (Created and populated at html documentation generation time) + └── helpfilesweb (Created and populated at md documentation generation time) +``` + +## Writing And Building The Documentation + +Part of the success of k\-Wave can be attributed to the good documentation, both of the individual functions and classes, and the examples. All code should be documented as outlined in the [Coding Standard](codingStandard.md). It can often be easiest to start with the [documentation template](helpfilesweb/classDocsExample.md). + +When adding a new class or function, examples should be added. If the code usage is relatively straightforward, examples can be included directly in the help documentation for that class or function. For more complex classes (e.g., the solver classes), longer tutorials or examples should be provided. + +1. **Tutorials:** These are worked examples stored as `.m` files in the `kwave.tutorials` name space. For tutorials, each block of code should be surrounded by a discussion guiding the user through the example. The discussion should be written using [publishing markup](https://uk.mathworks.com/help/matlab/matlab_prog/marking-up-matlab-comments-for-publishing.html)[*](#note). Similar to k\-Wave I, concepts introduced in other tutorials do not need to be re\-introduced. Try and focus on a relatively small number of new concepts in each tutorial. The tutorial code should generally run fast on basic hardware (< 1 min). +2. **Examples:** These are illustrative examples stored as `.m` files in the `kwave.examples` name space. Examples have a wider scope than tutorials, and may demonstrate a real\-world simulation using realistic grid sizes for example (so do not necessarily need to run fast). Examples should contain a comprehensive description of what the example does in the description of the file, but does not need to have long step\-by\-step. + +Please remember to also add the new class, function or tutorial/example file in the appropriate section header file (`Toolbox_Functions.m`, `Tutorials.m` or `Utility_Functions.m`). + +\* ***A note on publishing markup for tutorials and examples:*** +According to the matlab documentation, + +> _Markup only works in comments that immediately follow a section break._ + +This means that all the text within a section (starting with `%%`) has to come in one continuous area of `%`s in order for it to render. If executable code is interleaved with it, the rest of the text is not rendered. A workaround is to include the code we want to run in the comments area, and duplicate it after each block in order for it to run and produce the plots. Please have a look at the existing tutorials for examples. + +### Building The Documentation + +_MATLAB R2023b or later is required to build the documentation._ + +General and developer documentation that is static and does not have to be automatically generated from the code, should be written in markdown (`.md`) files inside the `/docs` folder. The in-code documentation is written in the `.m` files and can be automatically compiled by calling `kwave.devtools.GenerateDocumentation` inside MATLAB from the project root. This utility compiles the documentation into two formats: + +- `.html`, using [`publish`](https://uk.mathworks.com/help/matlab/ref/publish.html). After compiling, the `html` documentation can be found in `docs/helpfiles` and can be viewed by opening the MATLAB help browser and selecting **k-Wave II** from the list of supplemental software. +- `.md`, using [`export`](https://uk.mathworks.com/help/matlab/ref/export.html). After compiling, the `md` documentation can be found in `docs/helpfilesweb` and can be further processed with `mkdocs` to produce a standalone webpage in `readthedocs` style. + +To generate the standalone website with `mkdocs` after the `.md` files have been generated, you need to have a few packages in your python environment, so run `pip install -r requirements.txt` first. Then the website can be served locally with +``` +mkdocs serve --livereload +``` +and viewed in a browser in `http://127.0.0.1:8000/` for debugging purposes (watch the output of the above command for this address). + +## Logging And Errors + +Logging messages help users understand code settings and status, and help developers debug code. In k\-Wave\-II, all logging messages (including warnings and errors) should be printed using the `kwave.toolbox.Logger` class. This provides a consistent interface, allow adjusting the verbosity of the output messages, and allows piping the logging messages to an external file. + +## Toolboxes And External Code + +The core functionality of k\-Wave\-II should not depend on any MATLAB toolboxes. This is to minimise the requirements for non\-academic users who must purchase a MATLAB license to use k\-Wave. Tests can (and do) depend on additional toolboxes, as this dependency is fulfilled by the GitHub runners (both private and public). + + +If considering using other external code or libraries (e.g., from the file exchange), this should be flagged first on the issue or pull request. This way, a discussion can be had about the cost of introducing the dependency, and any potential licensing issues. + +## Testing Framework + +k\-Wave uses the [class\-based unit testing framework](https://uk.mathworks.com/help/matlab/class-based-unit-tests.html). There are several test levels (defined in `kwave.tests.TestType`), each of which lives in its own namespace: + +- **`+unit`:** Unit tests validate individual components of a function or class in isolation. +- **`+linting`:** Linting checks assess code for stylistic and syntactical correctness. +- **`+legacy`:** Legacy tests are regression tests against k\-Wave I to ensure existing functionality remains unaffected by changes. + +Each top level class or function should have at least one corresponding unit test. Unit tests should have 100% line coverage. Tests should inherit from one of the following: + +- `kwave.tests.unit.AbstractTestGridInput` for testing classes that derive from `kwave.toolbox.GridInput`. +- `kwave.tests.unit.AbstractTestGrid` for tests that need to iterate over different sized grids. +- `matlab.unittest.TestCase` for general tests. +- `matlab.perftest.TestCase` for performance tests. + +The filenames for all tests should start with `Test`. Unit tests should be named `TestClassName` or `TestFunctionName`. Other tests should be given sensible descriptive names. + + +The linting tests check for code complexity using [cylomatic complexity](https://uk.mathworks.com/help/matlab/matlab_prog/measure-code-complexity-using-cyclomatic-complexity.html), which is a measure of the decision structure complexity of the code. The complexity of all files changed in a pull request is automatically added to pull requests as part of the code checks action. While a particular number isn't enforced, both developers and reviewers should consider whether a re\-factoring is appropriate if the cylomatic complexity is above 10. + +To run the tests locally, call: + +- `kwave.tests.runTests(TestType=kwave.tests.TestType.unit)` +- `kwave.tests.runTests(TestType=kwave.tests.TestType.linting)` diff --git a/docs/git-model.png b/docs/git-model.png new file mode 100644 index 0000000000000000000000000000000000000000..5b96426c76e9b59f162331cf2c8eb5bd1446b8f6 GIT binary patch literal 124925 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�QQpl!FqPohLNfFE1 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

Kh5V?q&1SWLKGwoIdmjHtQyKHxgywu0?&pf* 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

|W@eX7X<*f|U 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 E57MRp_5c6? literal 0 HcmV?d00001 diff --git a/docs/maintainerDocs.md b/docs/maintainerDocs.md new file mode 100644 index 00000000..820a2652 --- /dev/null +++ b/docs/maintainerDocs.md @@ -0,0 +1,51 @@ +# Maintainer guidelines + +The [git-flow](https://nvie.com/posts/a-successful-git-branching-model/) workflow is summarised in the following graph, taken from Vincent Driessen's blog post. The only difference between this model and the one we are using in k-Wave-II, is that contributors do not commit changes directly into the feature branches, but they open Pull Requests (PRs) into them from their own forks of the repo. + +Toolbox releases distributed via Matlab are built from the `main` branch (`master` in the graph), and this is what most users are expected to use. If someone wants access to the latest features, they will need to check out and use the `develop` branch, therefore this branch should be in a usable state, even if it does not have the full, release-ready functionality. All other branches are for development purposes. + +![alt text](git-model.png "The git-flow workflow by Vincent Driessen") + + +## Before coding + +Maintainers should review issues regularly and place them appropriately in the repo [project board](https://github.com/orgs/ucl-bug/projects/3/views/1), to be worked on immediately or in the future. Any issues to be worked on should have corresponding feature or bugfix branches opened. + +Feature branches should branch off `develop` and bugfix branches off whichever is the most stable branch the bug is found in. + + +## Development + +All the branches of the repo are protected, meaning that contributors (including maintainers) cannot commit changes directly to them, they can only open PRs. Additionally, only maintainers can create branches in the main repo. + +Therefore all development is done in contributors' forks, on branches that they have branched off feature branches in their fork. When they are ready to merge their changes, they open a PR into the main repo's corresponding feature branch, and request a code review from the maintainers or code reviewers. + +When reviewing contributors' PRs, follow the checklist in the PR template, and check items off as you progress. You should check that + +- New tests, tutorials and examples are added as needed. +- All tests, existing or new, pass. +- Documentation is updated and builds correctly. + +You should also review the code changes themselves. Please be constructive and respectful in your comments and support contributors to improve their code. Once they address your concerns, resolve the conversation and when you are satisfied with the code changes overall, approve them. + +*If you think you need to take over development on someone else's PR, always ask them first.* + +Only maintainers can merge PRs into the feature branch. Merges need at least one approving review and all the automated tests to pass. You should also make sure that you have ticked all the checkboxes on the template. Once all this is done you can merge, using [squash merge](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-commits). Individual commits will still be accessible on GitHub in the (now closed) PR. + + + + +## Branch management + +Once a feature reaches a certain maturity, maintainers can choose to merge the related branch into `develop`, to let adventurous users try it out. You will probably have to resolve some conflicts at that point. + +Then development can continue in the feature branch, or if it is complete, the maintainers can lock the branch and prevent further commits to it. Feature branches should not be deleted (and cannot be deleted once locked). + +The same workflow described above applies to bug fixes. However, the fix should be merged into all the affected branches, not only the one that the bugfix branch branched off from. + + +## Releases + +When the time comes to prepare a new release, open a new release branch from `develop`. From that point on, no extra feature developments should go into the release branch, only bugfixes. Any extra tests needed for a release should be run on the release branch (e.g. compatibility with the newest and all previous Matlab releases we want to support), and the Changelog, documentation, and release notes for the website updated accordingly. + +Once the maintainers are satisfied with the release branch, squash merge it into the `main` branch, and add a new tag. You should also merge it back into `develop`, and any bugfixes into the affected feature branches as well. diff --git a/docs/test_doc_generation.ps1 b/docs/test_doc_generation.ps1 new file mode 100644 index 00000000..0e2b6c71 --- /dev/null +++ b/docs/test_doc_generation.ps1 @@ -0,0 +1,18 @@ +$n_html = (Get-ChildItem -Path "docs/helpfiles/" -Filter "*.html" -Recurse).Count +if ($n_html -eq 0) { + Write-Output "No html help files were generated" + return 1 +} + +$n_md = (Get-ChildItem -Path "docs/helpfilesweb/" -Filter "*.md" -Recurse | Where-Object { $_.Name -notlike "SUMMARY*" }).Count +if ($n_md -eq 0) { + Write-Output "No md documentation files were generated" + return 1 +} + +if ($n_html -ne $n_md) { + Write-Output "Number of html and md help files generated are not equal: $n_html vs $n_md" + return 1 +} + +Write-Output "$n_html html files generated, $n_md md files generated" diff --git a/docs/test_doc_generation.py b/docs/test_doc_generation.py new file mode 100644 index 00000000..2c85754e --- /dev/null +++ b/docs/test_doc_generation.py @@ -0,0 +1,18 @@ +from pathlib import Path +from os.path import isfile + +n_html = len([x for x in Path('./docs/helpfiles').rglob('*.html')]) +if n_html == 0: + print("No html help files were generated") + exit(1) + +n_md = len([x for x in Path('./docs/helpfilesweb').rglob('*.md') if ('SUMMARY' not in str(x)) ]) +if n_md == 0: + print("No md documentation files were generated") + exit(1) + +if n_html != n_md: + print("Number of html and md help files generated are not equal:", n_html, "vs", n_md) + exit(1) + +print(n_html, "html files generated,", n_md, "md files generated") diff --git a/docs/test_doc_generation.sh b/docs/test_doc_generation.sh new file mode 100644 index 00000000..59363a67 --- /dev/null +++ b/docs/test_doc_generation.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +n_html=`find docs/helpfiles/ -name "*.html" | wc -l` +if [ $n_html -eq 0 ]; then + echo "No html help files were generated" + return 1 +fi + +n_md=`find docs/helpfilesweb/ -name "*.md" | grep -v SUMMARY | wc -l` +if [ $n_md -eq 0 ]; then + echo "No md documentation files were generated" + return 1 +fi + +if [ $n_html -ne $n_md ]; then + echo "Number of html and md help files generated are not equal:" $n_html "vs" $n_md + return 1 +fi + +echo $n_html "html files generated," $n_md "md files generated" diff --git a/info.xml b/info.xml index 88ecff14..66fbc687 100644 --- a/info.xml +++ b/info.xml @@ -2,11 +2,11 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="optional"> - + 2022a k-Wave II toolbox - helpfiles + docs/helpfiles diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..b5f2ca29 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,34 @@ +site_name: k-Wave-II + +plugins: + - search + - literate-nav + +markdown_extensions: + - admonition + - pymdownx.arithmatex: + generic: true + +extra_javascript: + - https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js + - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js + +nav: + - Home: README.md + - Contributor guidelines: CONTRIBUTING.md + - Code of Conduct: codeOfConduct.md + - Licence: LICENSE.md + - How to: + - Tutorials: helpfilesweb/Tutorials/ + - Code reference: + - Toolbox Functions: helpfilesweb/Toolbox_Functions/ + - Utility Functions: helpfilesweb/Utility_Functions/ + - Developer docs: + - Developer intro: developerIntroduction.md + - Coding Standard: codingStandard.md + - Template Class: helpfilesweb/classDocsExample.md + - Maintainer guidelines: maintainerDocs.md + - Test Functions: helpfilesweb/Test_Functions/ + - Developer Tools: helpfilesweb/Developer_Tools/ + +theme: readthedocs diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..a47a26a6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +mkdocs +mkdocs-literate-nav +pymdown-extensions +pre-commit