├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── dependabot.yml └── workflows │ └── ci.yml ├── .github_changelog_generator ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── MANIFEST.in ├── README.md ├── fig.png ├── notebooks ├── Untitled.ipynb ├── Untitled1.ipynb ├── Untitled2.ipynb ├── examples.ipynb └── lightsheet.ipynb ├── pyproject.toml ├── setup.cfg ├── setup.py ├── src ├── _psfmodels-stubs │ └── __init__.pyi ├── _psfmodels │ ├── psfmath.h │ ├── pythonBindings.cpp │ ├── scalarPSF.cpp │ └── vectorialPSF.cpp └── psfmodels │ ├── __init__.py │ ├── _core.py │ ├── _cuvec.py │ ├── _jax_bessel.py │ ├── _napari.py │ ├── napari.yaml │ └── py.typed └── tests ├── test_napari_plugin.py ├── test_psf.py └── test_purepy.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ipynb linguist-detectable=false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * psfmodels version: 2 | * Python version: 3 | * Operating System: 4 | 5 | ### Description 6 | 7 | Describe what you were trying to get done. 8 | Tell us what happened, what went wrong, and what you expected to happen. 9 | 10 | ### What I Did 11 | 12 | ``` 13 | Paste the command(s) you ran and the output. 14 | If there was a crash, please include the traceback here. 15 | ``` 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | commit-message: 10 | prefix: "ci(dependabot):" 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "v*" 9 | pull_request: {} 10 | workflow_dispatch: 11 | 12 | jobs: 13 | test: 14 | name: ${{ matrix.platform }} (${{ matrix.python-version }}) 15 | runs-on: ${{ matrix.platform }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.7", "3.10", "3.11"] 20 | platform: [ubuntu-latest, macos-latest, windows-latest] 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - name: Set up Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@v5 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip 33 | python -m pip install .[testing] 34 | 35 | - name: Test with 36 | run: pytest -v --color=yes --cov=psfmodels --cov-report=xml 37 | 38 | - name: Coverage 39 | uses: codecov/codecov-action@v4 40 | with: 41 | token: ${{ secrets.CODECOV_TOKEN }} 42 | 43 | build: 44 | name: Build wheels on ${{ matrix.os }} 45 | runs-on: ${{ matrix.os }} 46 | strategy: 47 | fail-fast: false 48 | matrix: 49 | os: [ubuntu-20.04, windows-2019, macos-11] 50 | 51 | steps: 52 | - uses: actions/checkout@v4 53 | 54 | - uses: actions/setup-python@v5 55 | name: Install Python 56 | with: 57 | python-version: "3.10" 58 | 59 | - name: Build wheels 60 | uses: pypa/cibuildwheel@v2.16.5 61 | 62 | - uses: actions/upload-artifact@v4 63 | with: 64 | path: ./wheelhouse/*.whl 65 | 66 | - name: Build sdist 67 | if: matrix.os == 'ubuntu-20.04' 68 | run: | 69 | python -m pip install build 70 | python -m build --sdist 71 | 72 | - uses: actions/upload-artifact@v4 73 | if: matrix.os == 'ubuntu-20.04' 74 | with: 75 | path: dist/*.tar.gz 76 | 77 | upload_pypi: 78 | needs: [build] 79 | runs-on: ubuntu-latest 80 | if: "success() && startsWith(github.ref, 'refs/tags/')" 81 | steps: 82 | - uses: actions/download-artifact@v4 83 | with: 84 | name: artifact 85 | path: dist 86 | 87 | - uses: pypa/gh-action-pypi-publish@v1.8.11 88 | with: 89 | user: __token__ 90 | password: ${{ secrets.pypi_token }} 91 | 92 | - uses: softprops/action-gh-release@v1 93 | with: 94 | generate_release_notes: true 95 | -------------------------------------------------------------------------------- /.github_changelog_generator: -------------------------------------------------------------------------------- 1 | user=tlambert03 2 | project=psfmodels 3 | issues=false 4 | exclude-labels=duplicate,question,invalid,wontfix,hide 5 | add-sections={"tests":{"prefix":"**Tests & CI:**","labels":["tests"]}} 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | 104 | # IDE settings 105 | .vscode/ 106 | *.DS_Store 107 | 108 | src/psfmodels/_version.py 109 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | autoupdate_schedule: quarterly 3 | autofix_commit_msg: "style(pre-commit.ci): auto fixes [...]" 4 | autoupdate_commit_msg: "ci(pre-commit.ci): autoupdate" 5 | 6 | repos: 7 | - repo: https://github.com/abravalheri/validate-pyproject 8 | rev: v0.12.2 9 | hooks: 10 | - id: validate-pyproject 11 | 12 | - repo: https://github.com/charliermarsh/ruff-pre-commit 13 | rev: v0.0.265 14 | hooks: 15 | - id: ruff 16 | args: [--fix] 17 | 18 | - repo: https://github.com/psf/black 19 | rev: 23.3.0 20 | hooks: 21 | - id: black 22 | 23 | - repo: https://github.com/pre-commit/mirrors-mypy 24 | rev: v1.2.0 25 | hooks: 26 | - id: mypy 27 | files: "^src/" 28 | additional_dependencies: 29 | - numpy 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include src *.cpp *.h 2 | recursive-include src *.yaml 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # psfmodels 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/psfmodels.svg?color=green)](https://pypi.org/project/psfmodels) 4 | [![Python 5 | Version](https://img.shields.io/pypi/pyversions/psfmodels.svg?color=green)](https://python.org) 6 | [![CI](https://github.com/tlambert03/psfmodels/actions/workflows/ci.yml/badge.svg)](https://github.com/tlambert03/psfmodels/actions/workflows/ci.yml) 7 | [![codecov](https://codecov.io/gh/tlambert03/psfmodels/branch/main/graph/badge.svg)](https://codecov.io/gh/tlambert03/psfmodels) 8 | 9 | Python bindings for scalar and vectorial models of the point spread function. 10 | 11 | Original C++ code and MATLAB MEX bindings Copyright © 2006-2013, [Francois 12 | Aguet](http://www.francoisaguet.net/software.html), distributed under GPL-3.0 13 | license. Python bindings by Talley Lambert 14 | 15 | This package contains three models: 16 | 17 | 1. The vectorial model is described in Auget et al 20091. For more 18 | information and implementation details, see Francois' Thesis2. 19 | 2. A scalar model, based on Gibson & Lanni3. 20 | 3. A gaussian approximation (both paraxial and non-paraxial), using paramters from Zhang et al (2007)4. 21 | 22 | 23 | 24 | 1 [F. Aguet et al., (2009) Opt. Express 17(8), pp. 25 | 6829-6848](https://doi.org/10.1364/OE.17.006829) 26 | 27 | 2 [F. Aguet. (2009) Super-Resolution Fluorescence Microscopy Based on 28 | Physical Models. Swiss Federal Institute of Technology Lausanne, EPFL Thesis no. 29 | 4418](http://bigwww.epfl.ch/publications/aguet0903.html) 30 | 31 | 3 [F. Gibson and F. Lanni (1992) J. Opt. Soc. Am. A, vol. 9, no. 1, pp. 154-166](https://opg.optica.org/josaa/abstract.cfm?uri=josaa-9-1-154) 32 | 33 | 4 [Zhang et al (2007). Appl Opt 34 | . 2007 Apr 1;46(10):1819-29.](https://doi.org/10.1364/AO.46.001819) 35 | 36 | 37 | 38 | ### see also: 39 | 40 | For a different (faster) scalar-based Gibson–Lanni PSF model, see the 41 | [MicroscPSF](https://github.com/MicroscPSF) project, based on [Li et al 42 | (2017)](https://doi.org/10.1364/JOSAA.34.001029) which has been implemented in 43 | [Python](https://github.com/MicroscPSF/MicroscPSF-Py), 44 | [MATLAB](https://github.com/MicroscPSF/MicroscPSF-Matlab), and 45 | [ImageJ/Java](https://github.com/MicroscPSF/MicroscPSF-ImageJ) 46 | 47 | ## Install 48 | 49 | ```sh 50 | pip install psfmodels 51 | ``` 52 | 53 | ### from source 54 | 55 | ```sh 56 | git clone https://github.com/tlambert03/PSFmodels.git 57 | cd PSFmodels 58 | pip install -e ".[dev]" # will compile c code via pybind11 59 | ``` 60 | 61 | ## Usage 62 | 63 | There are two main functions in `psfmodels`: `vectorial_psf` and `scalar_psf`. 64 | Additionally, each version has a helper function called `vectorial_psf_centered` 65 | and `scalar_psf_centered` respectively. The main difference is that the `_psf` 66 | functions accept a vector of Z positions `zv` (relative to coverslip) at which 67 | PSF is calculated. As such, the point source may or may not actually be in the 68 | center of the rendered volume. The `_psf_centered` variants, by contrast, do 69 | _not_ accecpt `zv`, but rather accept `nz` (the number of z planes) and `dz` 70 | (the z step size in microns), and always generates an output volume in which the 71 | point source is positioned in the middle of the Z range, with planes equidistant 72 | from each other. All functions accept an argument `pz`, specifying the position 73 | of the point source relative to the coverslip. See additional keyword arguments 74 | below 75 | 76 | _Note, all output dimensions (`nx` and `nz`) should be odd._ 77 | 78 | ```python 79 | import psfmodels as psfm 80 | import matplotlib.pyplot as plt 81 | from matplotlib.colors import PowerNorm 82 | 83 | # generate centered psf with a point source at `pz` microns from coverslip 84 | # shape will be (127, 127, 127) 85 | psf = psfm.make_psf(127, 127, dxy=0.05, dz=0.05, pz=0) 86 | fig, (ax1, ax2) = plt.subplots(1, 2) 87 | ax1.imshow(psf[nz//2], norm=PowerNorm(gamma=0.4)) 88 | ax2.imshow(psf[:, nx//2], norm=PowerNorm(gamma=0.4)) 89 | plt.show() 90 | ``` 91 | 92 | ![Image of PSF](fig.png) 93 | 94 | ```python 95 | # instead of nz and dz, you can directly specify a vector of z positions 96 | import numpy as np 97 | 98 | # generate 31 evenly spaced Z positions from -3 to 3 microns 99 | psf = psfm.make_psf(np.linspace(-3, 3, 31), nx=127) 100 | psf.shape # (31, 127, 127) 101 | ``` 102 | 103 | **all** PSF functions accept the following parameters. Units should be provided 104 | in microns unless otherwise stated. Python API may change slightly in the 105 | future. See function docstrings as well. 106 | 107 | ``` 108 | nx (int): XY size of output PSF in pixels, must be odd. 109 | dxy (float): pixel size in sample space (microns) [default: 0.05] 110 | pz (float): depth of point source relative to coverslip (in microns) [default: 0] 111 | ti0 (float): working distance of the objective (microns) [default: 150.0] 112 | ni0 (float): immersion medium refractive index, design value [default: 1.515] 113 | ni (float): immersion medium refractive index, experimental value [default: 1.515] 114 | tg0 (float): coverslip thickness, design value (microns) [default: 170.0] 115 | tg (float): coverslip thickness, experimental value (microns) [default: 170.0] 116 | ng0 (float): coverslip refractive index, design value [default: 1.515] 117 | ng (float): coverslip refractive index, experimental value [default: 1.515] 118 | ns (float): sample refractive index [default: 1.47] 119 | wvl (float): emission wavelength (microns) [default: 0.6] 120 | NA (float): numerical aperture [default: 1.4] 121 | ``` 122 | 123 | ## Comparison with other models 124 | 125 | While these models are definitely slower than the one implemented in [Li et al 126 | (2017)](https://doi.org/10.1364/JOSAA.34.001029) and 127 | [MicroscPSF](https://github.com/MicroscPSF), there are some interesting 128 | differences between the scalar and vectorial approximations, particularly with 129 | higher NA lenses, non-ideal sample refractive index, and increasing spherical 130 | aberration with depth from the coverslip. 131 | 132 | For an interactive comparison, see the [examples.ipynb](notebooks/examples.ipynb) Jupyter 133 | notebook. 134 | 135 | ## Lightsheet PSF utility function 136 | 137 | The `psfmodels.tot_psf()` function provides a quick way to simulate the total 138 | system PSF (excitation x detection) as might be observed on a light sheet 139 | microscope (currently, only strictly orthogonal illumination and detection are 140 | supported). See the [lightsheet.ipynb](notebooks/lightsheet.ipynb) Jupyter notebook for 141 | examples. 142 | -------------------------------------------------------------------------------- /fig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tlambert03/PSFmodels/dfe2b6f2e829ef6351c75c5995e7fcf7b3ef2edc/fig.png -------------------------------------------------------------------------------- /notebooks/Untitled1.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import numpy as np\n", 10 | "import cupy as cp\n", 11 | "import matplotlib.pyplot as plt\n", 12 | "from matplotlib.patches import Circle, Wedge\n", 13 | "from matplotlib.collections import PatchCollection, LineCollection" 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": 20, 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "xystep = 50 * 1e-9\n", 23 | "NA = 1.45\n", 24 | "sf = 3\n", 25 | "\n", 26 | "\n", 27 | "# params\n", 28 | "ti0 = 0.000190 # working distance\n", 29 | "ni0 = 1.518\n", 30 | "ni = 1.518\n", 31 | "tg0 = 0.000170\n", 32 | "tg = 0.000170\n", 33 | "ng0 = 1.515\n", 34 | "ng = 1.515\n", 35 | "ns = 1.33\n", 36 | "lamda = 550 * 1e-9\n", 37 | "\n", 38 | "\n", 39 | "# precompute some stuff\n", 40 | "k0 = 2 * np.pi / lamda # angular wave number\n", 41 | "ni0_2 = ni0**2\n", 42 | "ni_2 = ni**2\n", 43 | "ng0_2 = ng0**2\n", 44 | "ng_2 = ng**2\n", 45 | "ns_2 = ns**2\n", 46 | "NA_2 = NA**2\n", 47 | "\n", 48 | "\n", 49 | "nx = 31\n", 50 | "zp, yp, xp = (0, 0, 0)" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": 21, 56 | "metadata": {}, 57 | "outputs": [ 58 | { 59 | "data": { 60 | "image/png": "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\n", 61 | "text/plain": [ 62 | "
" 63 | ] 64 | }, 65 | "metadata": { 66 | "needs_background": "light" 67 | }, 68 | "output_type": "display_data" 69 | } 70 | ], 71 | "source": [ 72 | "alpha = np.arcsin(NA / ni) # the half angle of collection\n", 73 | "\n", 74 | "fig, ax = plt.subplots()\n", 75 | "patches = [Wedge((0,0), 1, 90 - np.rad2deg(alpha), 90 + np.rad2deg(alpha), width=0.05)]\n", 76 | "lines = [[(0, 0), (np.sin(alpha), np.cos(alpha))],[(0, 0), (-np.sin(alpha), np.cos(alpha))]]\n", 77 | "ax.add_collection(PatchCollection(patches))\n", 78 | "ax.add_collection(LineCollection(lines, colors='k', linewidths=2))\n", 79 | "ax.set_xlim([-1, 1])\n", 80 | "ax.set_ylim([0, 1])\n", 81 | "ax.set_aspect('equal')\n", 82 | "ax.set_title('maximum collection angle')\n", 83 | "plt.show()" 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": 26, 89 | "metadata": {}, 90 | "outputs": [], 91 | "source": [ 92 | "# xymax = maximum xy pixels to calculate\n", 93 | "xymax = (nx * sf - 1) // 2\n", 94 | "xp *= sf / xystep\n", 95 | "yp *= sf / xystep\n", 96 | "# add some more in case the particle is offcentered\n", 97 | "rn = 1 + int(np.hypot(xp, yp))\n", 98 | "# rmax = the maximum radius we need to calculate\n", 99 | "# +1 for interpolation, dx, dy\n", 100 | "rmax = int(np.ceil(np.sqrt(2.0) * xymax) + rn + 1)\n", 101 | "# rvec are the radii in spatial units (m) at which we will calculate the integral\n", 102 | "rvec = cp.arange(rmax) * xystep / sf\n", 103 | "constJ = k0 * rvec * ni # mnumber of wavelengths from center?\n", 104 | "ci = zp * (1.0 - ni / ns) + ni * (tg0 / ng0 + ti0 / ni0 - tg / ng)" 105 | ] 106 | }, 107 | { 108 | "cell_type": "code", 109 | "execution_count": 27, 110 | "metadata": {}, 111 | "outputs": [ 112 | { 113 | "name": "stdout", 114 | "output_type": "stream", 115 | "text": [ 116 | "xymax: 46 pixels\n", 117 | "rmax: 68 pixles\n" 118 | ] 119 | } 120 | ], 121 | "source": [ 122 | "print(f\"xymax: {xymax} pixels\")\n", 123 | "print(f\"rmax: {rmax} pixles\")" 124 | ] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": null, 129 | "metadata": {}, 130 | "outputs": [], 131 | "source": [] 132 | } 133 | ], 134 | "metadata": { 135 | "kernelspec": { 136 | "display_name": "Python 3", 137 | "language": "python", 138 | "name": "python3" 139 | }, 140 | "language_info": { 141 | "codemirror_mode": { 142 | "name": "ipython", 143 | "version": 3 144 | }, 145 | "file_extension": ".py", 146 | "mimetype": "text/x-python", 147 | "name": "python", 148 | "nbconvert_exporter": "python", 149 | "pygments_lexer": "ipython3", 150 | "version": "3.7.3" 151 | } 152 | }, 153 | "nbformat": 4, 154 | "nbformat_minor": 4 155 | } 156 | -------------------------------------------------------------------------------- /notebooks/Untitled2.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "%load_ext autoreload\n", 10 | "%autoreload 2\n", 11 | "\n", 12 | "import perfplot\n", 13 | "import numpy as np\n", 14 | "from psfmodels.cuvec import vectorial_psf as cu_vpsf\n", 15 | "from psfmodels import vectorial_psf as c_vpsf\n", 16 | "from microscPSF.microscPSF import gLXYZFocalScan" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": 2, 22 | "metadata": {}, 23 | "outputs": [], 24 | "source": [ 25 | "\n", 26 | "zrange = 3\n", 27 | "dxy = 0.05\n", 28 | "wvl = 0.550\n", 29 | "\n", 30 | "sf = 3\n", 31 | "params = {\n", 32 | " 'NA': 1.45,\n", 33 | " 'ti0' : 190,\n", 34 | " 'ni0' : 1.518,\n", 35 | " 'ni' : 1.518,\n", 36 | " 'tg0' : 170,\n", 37 | " 'tg' : 170,\n", 38 | " 'ng0' : 1.5150,\n", 39 | " 'ng' : 1.5150,\n", 40 | " 'ns' : 1.515,\n", 41 | " 'M' : 1,\n", 42 | " 'NA' : 1.4500,\n", 43 | " 'zd0': 200.0 * 1.0e+3\n", 44 | "}\n", 45 | "\n", 46 | "def microscpsf(nx):\n", 47 | " zv = np.linspace(-zrange/2, zrange/2, nx//2)\n", 48 | " return gLXYZFocalScan(params, dxy, nx, zv, wvl=wvl).shape\n", 49 | "\n", 50 | "def cuda_vec(nx):\n", 51 | " zv = np.linspace(-zrange/2, zrange/2, nx//2)\n", 52 | " return cu_vpsf(zv, nx=nx, dxy=dxy, params=params, wvl=wvl).shape\n", 53 | "\n", 54 | "def c_vec(nx):\n", 55 | " zv = np.linspace(-zrange/2, zrange/2, nx//2)\n", 56 | " return c_vpsf(zv, nx=nx, dxy=dxy, params=params, wvl=wvl).shape" 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": 4, 62 | "metadata": {}, 63 | "outputs": [ 64 | { 65 | "name": "stderr", 66 | "output_type": "stream", 67 | "text": [ 68 | "\n", 69 | "\n", 70 | " 0%| | 0/5 [00:00" 153 | ] 154 | }, 155 | "metadata": { 156 | "needs_background": "light" 157 | }, 158 | "output_type": "display_data" 159 | } 160 | ], 161 | "source": [ 162 | "bench = perfplot.bench(\n", 163 | " setup=lambda n: n,\n", 164 | " kernels=[microscpsf, cuda_vec, c_vec],\n", 165 | " n_range=[31, 63, 127, 511, 1023],\n", 166 | " logx=True,\n", 167 | " logy=True,\n", 168 | " xlabel='nx (nz = nx // 2)'\n", 169 | ")\n", 170 | "bench.show()" 171 | ] 172 | }, 173 | { 174 | "cell_type": "code", 175 | "execution_count": null, 176 | "metadata": {}, 177 | "outputs": [], 178 | "source": [] 179 | } 180 | ], 181 | "metadata": { 182 | "kernelspec": { 183 | "display_name": "Python 3", 184 | "language": "python", 185 | "name": "python3" 186 | }, 187 | "language_info": { 188 | "codemirror_mode": { 189 | "name": "ipython", 190 | "version": 3 191 | }, 192 | "file_extension": ".py", 193 | "mimetype": "text/x-python", 194 | "name": "python", 195 | "nbconvert_exporter": "python", 196 | "pygments_lexer": "ipython3", 197 | "version": "3.7.3" 198 | } 199 | }, 200 | "nbformat": 4, 201 | "nbformat_minor": 4 202 | } 203 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel", "setuptools-scm", "pybind11>=2.8.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.setuptools_scm] 6 | write_to = "src/psfmodels/_version.py" 7 | 8 | [tool.check-manifest] 9 | ignore = [ 10 | ".github_changelog_generator", 11 | ".pre-commit-config.yaml", 12 | "fig.png", 13 | "notebooks/*", 14 | "tests/*", 15 | 'src/psfmodels/_version.py', 16 | ] 17 | 18 | [tool.ruff] 19 | line-length = 88 20 | target-version = "py37" 21 | # https://beta.ruff.rs/docs/rules/ 22 | select = [ 23 | "E", # style errors 24 | "W", # style warnings 25 | "F", # flakes 26 | "D", # pydocstyle 27 | "I", # isort 28 | "UP", # pyupgrade 29 | "C", # flake8-comprehensions 30 | "B", # flake8-bugbear 31 | "A001", # flake8-builtins 32 | "RUF", # ruff-specific rules 33 | ] 34 | # I do this to get numpy-style docstrings AND retain 35 | # D417 (Missing argument descriptions in the docstring) 36 | # otherwise, see: 37 | # https://beta.ruff.rs/docs/faq/#does-ruff-support-numpy-or-google-style-docstrings 38 | # https://github.com/charliermarsh/ruff/issues/2606 39 | ignore = [ 40 | "D100", # Missing docstring in public module 41 | "D107", # Missing docstring in __init__ 42 | "D203", # 1 blank line required before class docstring 43 | "D212", # Multi-line docstring summary should start at the first line 44 | "D213", # Multi-line docstring summary should start at the second line 45 | "D401", # First line should be in imperative mood 46 | "D413", # Missing blank line after last section 47 | "D416", # Section name should end with a colon 48 | ] 49 | 50 | [tool.ruff.per-file-ignores] 51 | "tests/*.py" = ["D"] 52 | "setup.py" = ["D"] 53 | 54 | # https://mypy.readthedocs.io/en/stable/config_file.html 55 | [tool.mypy] 56 | files = "src/**/" 57 | # strict = true 58 | disallow_any_generics = false 59 | disallow_subclassing_any = false 60 | show_error_codes = true 61 | pretty = true 62 | 63 | # # module specific overrides 64 | # [[tool.mypy.overrides]] 65 | # module = ["numpy.*",] 66 | # ignore_errors = true 67 | 68 | # https://docs.pytest.org/en/6.2.x/customize.html 69 | [tool.pytest.ini_options] 70 | minversion = "6.0" 71 | testpaths = ["tests"] 72 | filterwarnings = ["error"] 73 | 74 | [tool.cibuildwheel] 75 | # Skip 32-bit builds & PyPy wheels on all platforms 76 | skip = ["*-win32", "pp*", "*i686"] 77 | test-extras = ["testing"] 78 | test-command = "pytest {project}/tests -v" 79 | test-skip = "*-musllinux*" 80 | 81 | [tool.cibuildwheel.macos] 82 | archs = ["x86_64", "arm64"] 83 | 84 | # https://coverage.readthedocs.io/en/6.4/config.html 85 | [tool.coverage.report] 86 | exclude_lines = [ 87 | "pragma: no cover", 88 | "if TYPE_CHECKING:", 89 | "@overload", 90 | "except ImportError", 91 | "\\.\\.\\.", 92 | "raise NotImplementedError()", 93 | ] 94 | [tool.coverage.run] 95 | source = ["src"] 96 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = psfmodels 3 | description = Scalar and vectorial models of the microscope point spread function (PSF). 4 | long_description = file: README.md 5 | long_description_content_type = text/markdown 6 | url = https://github.com/tlambert03/psfmodels 7 | author = Talley Lambert 8 | author_email = talley.lambert@gmail.com 9 | license = GPL-3.0 10 | license_file = LICENSE 11 | classifiers = 12 | Development Status :: 3 - Alpha 13 | Framework :: napari 14 | License :: OSI Approved :: GNU General Public License v3 (GPLv3) 15 | Natural Language :: English 16 | Programming Language :: Python :: 3 17 | Programming Language :: Python :: 3 :: Only 18 | Programming Language :: Python :: 3.7 19 | Programming Language :: Python :: 3.8 20 | Programming Language :: Python :: 3.9 21 | Programming Language :: Python :: 3.10 22 | project_urls = 23 | Source Code =https://github.com/tlambert03/psfmodels 24 | 25 | [options] 26 | packages = 27 | psfmodels 28 | _psfmodels-stubs 29 | install_requires = 30 | numpy 31 | scipy>=0.14.0 32 | typing-extensions 33 | python_requires = >=3.7 34 | include_package_data = True 35 | package_dir = 36 | =src 37 | zip_safe = False 38 | 39 | [options.packages.find] 40 | where = src 41 | 42 | [options.entry_points] 43 | napari.manifest = 44 | psfmodels = psfmodels:napari.yaml 45 | 46 | [options.extras_require] 47 | dev = 48 | black 49 | flake8 50 | flake8-docstrings 51 | flake8-typing-imports 52 | ipython 53 | isort 54 | mypy 55 | pre-commit 56 | pydocstyle 57 | pytest 58 | pytest-cov 59 | tox 60 | tox-conda 61 | testing = 62 | pytest 63 | pytest-cov 64 | jax 65 | magicgui;platform_system!="Linux" 66 | pyside2;platform_system!="Linux" and python_version<"3.11" 67 | qtpy;platform_system!="Linux" 68 | 69 | [options.package_data] 70 | * = *.pyi, py.typed 71 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pybind11.setup_helpers import Pybind11Extension 2 | from setuptools import setup 3 | 4 | ext_modules = [ 5 | Pybind11Extension("_psfmodels", ["src/_psfmodels/pythonBindings.cpp"], cxx_std=17) 6 | ] 7 | 8 | setup(ext_modules=ext_modules) 9 | -------------------------------------------------------------------------------- /src/_psfmodels-stubs/__init__.pyi: -------------------------------------------------------------------------------- 1 | """Scalar and Vectorial PSF Models.""" 2 | import numpy 3 | import numpy.typing as npt 4 | 5 | __all__ = ["scalar_psf", "vectorial_psf", "vectorial_psf_deriv"] 6 | 7 | def scalar_psf( 8 | zv: npt.NDArray[numpy.float64], 9 | nx: int, 10 | pz: float, 11 | ti0: float, 12 | ni0: float, 13 | ni: float, 14 | tg0: float, 15 | tg: float, 16 | ng0: float, 17 | ng: float, 18 | ns: float, 19 | wvl: float, 20 | NA: float, 21 | dxy: float, 22 | sf: int = 3, 23 | mode: int = 1, 24 | ) -> npt.NDArray[numpy.float64]: 25 | """Compute scalar PSF model described by Gibson and Lanni. 26 | 27 | The model is described in F. Aguet et al., Opt. Express 17(8), pp. 6829-6848, 2009 28 | For more information and implementation details, see F. Aguet, Ph.D Thesis, Swiss 29 | Federal Institute of Technology, Lausanne (EPFL), 2009 30 | 31 | C++ code by Francois Aguet, 2009. Python bindings by Talley Lambert, 2019. 32 | 33 | Parameters 34 | ---------- 35 | zv : np.ndarray 36 | Vector of Z positions at which PSF is calculated (in microns, relative to 37 | coverslip) 38 | nx : int 39 | XY size of output PSF in pixels, must be odd. 40 | pz : float 41 | point source z position above the coverslip in microns. 42 | ti0 : float 43 | working distance of the objective (microns) 44 | ni0 : float 45 | immersion medium refractive index, design value 46 | ni : float 47 | immersion medium refractive index, experimental value 48 | tg0 : float 49 | coverslip thickness, design value (microns) 50 | tg : float 51 | coverslip thickness, experimental value (microns) 52 | ng0 : float 53 | coverslip refractive index, design value 54 | ng : float 55 | coverslip refractive index, experimental value 56 | ns : float 57 | sample refractive index 58 | wvl : float 59 | emission wavelength (microns) 60 | NA : float 61 | numerical aperture 62 | dxy : float 63 | pixel size in sample space (microns) 64 | sf : int, optional 65 | oversampling factor to approximate pixel integration, by default 3 66 | mode : int, optional 67 | if 0, returns oversampled PSF, by default 1 68 | 69 | Returns 70 | ------- 71 | npt.NDArray[numpy.float64] 72 | PSF with type np.float64 and shape (len(zv), nx, nx) 73 | """ 74 | 75 | def vectorial_psf( 76 | zv: npt.NDArray[numpy.float64], 77 | nx: int, 78 | pz: float, 79 | ti0: float, 80 | ni0: float, 81 | ni: float, 82 | tg0: float, 83 | tg: float, 84 | ng0: float, 85 | ng: float, 86 | ns: float, 87 | wvl: float, 88 | NA: float, 89 | dxy: float, 90 | sf: int = 3, 91 | mode: int = 1, 92 | ) -> npt.NDArray[numpy.float64]: 93 | """Compute vectorial microscope point spread function model. 94 | 95 | The model is described in F. Aguet et al., Opt. Express 17(8), pp. 6829-6848, 2009 96 | For more information and implementation details, see F. Aguet, Ph.D Thesis, Swiss 97 | Federal Institute of Technology, Lausanne (EPFL), 2009 98 | 99 | C++ code by Francois Aguet, 2009. Python bindings by Talley Lambert, 2019. 100 | 101 | Parameters 102 | ---------- 103 | zv : np.ndarray 104 | Vector of Z positions at which PSF is calculated (in microns, relative to 105 | coverslip) 106 | nx : int 107 | XY size of output PSF in pixels, must be odd. 108 | pz : float 109 | point source z position above the coverslip in microns. 110 | ti0 : float 111 | working distance of the objective (microns) 112 | ni0 : float 113 | immersion medium refractive index, design value 114 | ni : float 115 | immersion medium refractive index, experimental value 116 | tg0 : float 117 | coverslip thickness, design value (microns) 118 | tg : float 119 | coverslip thickness, experimental value (microns) 120 | ng0 : float 121 | coverslip refractive index, design value 122 | ng : float 123 | coverslip refractive index, experimental value 124 | ns : float 125 | sample refractive index 126 | wvl : float 127 | emission wavelength (microns) 128 | NA : float 129 | numerical aperture 130 | dxy : float 131 | pixel size in sample space (microns) 132 | sf : int, optional 133 | oversampling factor to approximate pixel integration, by default 3 134 | mode : int, optional 135 | if 0, returns oversampled PSF, by default 1 136 | 137 | Returns 138 | ------- 139 | npt.NDArray[numpy.float64] 140 | PSF with type np.float64 and shape (len(zv), nx, nx) 141 | """ 142 | 143 | def vectorial_psf_deriv( 144 | pixdxp: npt.NDArray[numpy.float64], 145 | pixdyp: npt.NDArray[numpy.float64], 146 | pixdzp: npt.NDArray[numpy.float64], 147 | zv: npt.NDArray[numpy.float64], 148 | nx: int, 149 | pz: float, 150 | ti0: float, 151 | ni0: float, 152 | ni: float, 153 | tg0: float, 154 | tg: float, 155 | ng0: float, 156 | ng: float, 157 | ns: float, 158 | wvl: float, 159 | NA: float, 160 | dxy: float, 161 | sf: int = 3, 162 | mode: int = 1, 163 | ) -> npt.NDArray[numpy.float64]: 164 | """Compute vectorial point spread function model, and return derivatives. 165 | 166 | Parameters 167 | ---------- 168 | pixdxp : npt.NDArray[numpy.float64] 169 | Derivative of pixel x position with respect to z position 170 | pixdyp : npt.NDArray[numpy.float64] 171 | Derivative of pixel y position with respect to z position 172 | pixdzp : npt.NDArray[numpy.float64] 173 | Derivative of pixel z position with respect to z position 174 | zv : np.ndarray 175 | Vector of Z positions at which PSF is calculated (in microns, relative to 176 | coverslip) 177 | nx : int 178 | XY size of output PSF in pixels, must be odd. 179 | pz : float 180 | point source z position above the coverslip in microns. 181 | ti0 : float 182 | working distance of the objective (microns) 183 | ni0 : float 184 | immersion medium refractive index, design value 185 | ni : float 186 | immersion medium refractive index, experimental value 187 | tg0 : float 188 | coverslip thickness, design value (microns) 189 | tg : float 190 | coverslip thickness, experimental value (microns) 191 | ng0 : float 192 | coverslip refractive index, design value 193 | ng : float 194 | coverslip refractive index, experimental value 195 | ns : float 196 | sample refractive index 197 | wvl : float 198 | emission wavelength (microns) 199 | NA : float 200 | numerical aperture 201 | dxy : float 202 | pixel size in sample space (microns) 203 | sf : int, optional 204 | oversampling factor to approximate pixel integration, by default 3 205 | mode : int, optional 206 | if 0, returns oversampled PSF, by default 1 207 | 208 | Returns 209 | ------- 210 | numpy.ndarray 211 | PSF with type np.float64 and shape (len(zv), nx, nx) 212 | """ 213 | -------------------------------------------------------------------------------- /src/_psfmodels/psfmath.h: -------------------------------------------------------------------------------- 1 | /* psfmath.h contains functions required for the calculation of point spread 2 | * function models. 3 | * 4 | * Copyright (C) 2005-2013 Francois Aguet 5 | * 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 3 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | */ 20 | 21 | #ifndef PSFMATH_H 22 | #define PSFMATH_H 23 | 24 | #include 25 | #define PI 3.14159265358979311599796346854 26 | 27 | namespace std 28 | { 29 | 30 | typedef struct 31 | { 32 | double ti0, ni0, ni0_2, ni, ni_2, tg0, tg, ng0, ng0_2, ng, ng_2, ns, ns_2, 33 | lambda, k0, dxy, NA, NA_2, alpha; 34 | int sf, mode; 35 | } parameters; 36 | 37 | // Constants for polynomial Bessel function approximation from [Abramowitz (p. 38 | // 369)] 39 | const double j0c[7] = {1, -2.2499997, 1.2656208, -0.3163866, 40 | 0.0444479, -0.0039444, 0.0002100}; 41 | const double t0c[7] = {-.78539816, -.04166397, -.00003954, 0.00262573, 42 | -.00054125, -.00029333, .00013558}; 43 | const double f0c[7] = {.79788456, -0.00000077, -.00552740, -.00009512, 44 | 0.00137237, -0.00072805, 0.00014476}; 45 | const double j1c[7] = {0.5, -0.56249985, 0.21093573, -0.03954289, 46 | 0.00443319, -0.00031761, 0.00001109}; 47 | const double f1c[7] = {0.79788456, 0.00000156, 0.01659667, 0.00017105, 48 | -0.00249511, 0.00113653, -0.00020033}; 49 | const double t1c[7] = {-2.35619449, 0.12499612, 0.00005650, -0.00637897, 50 | 0.00074348, 0.00079824, -0.00029166}; 51 | 52 | // Bessel functions J0(x) and J1(x) 53 | // Uses the polynomial approximations on p. 369-70 of Abramowitz & Stegun 54 | // (1972). The error in J0 is supposed to be less than or equal to 5 x 10^-8. 55 | __inline double J0(double x) 56 | { 57 | double r; 58 | 59 | if (x < 0.0) 60 | x *= -1.0; 61 | 62 | if (x <= 3.0) 63 | { 64 | double y = x * x / 9.0; 65 | r = j0c[0] + 66 | y * (j0c[1] + 67 | y * (j0c[2] + 68 | y * (j0c[3] + y * (j0c[4] + y * (j0c[5] + y * j0c[6]))))); 69 | } 70 | else 71 | { 72 | double y = 3.0 / x; 73 | double theta0 = 74 | x + t0c[0] + 75 | y * (t0c[1] + 76 | y * (t0c[2] + 77 | y * (t0c[3] + y * (t0c[4] + y * (t0c[5] + y * t0c[6]))))); 78 | double f0 = 79 | f0c[0] + 80 | y * (f0c[1] + 81 | y * (f0c[2] + 82 | y * (f0c[3] + y * (f0c[4] + y * (f0c[5] + y * f0c[6]))))); 83 | r = sqrt(1.0 / x) * f0 * cos(theta0); 84 | } 85 | return r; 86 | } 87 | 88 | __inline double J1(double x) 89 | { 90 | double r; 91 | double sign = 1.0; 92 | if (x < 0.0) 93 | { 94 | x *= -1.0; 95 | sign *= -1.0; 96 | } 97 | if (x <= 3.0) 98 | { 99 | double y = x * x / 9.0; 100 | r = x * 101 | (j1c[0] + 102 | y * (j1c[1] + 103 | y * (j1c[2] + 104 | y * (j1c[3] + y * (j1c[4] + y * (j1c[5] + y * j1c[6])))))); 105 | } 106 | else 107 | { 108 | double y = 3.0 / x; 109 | double theta1 = 110 | x + t1c[0] + 111 | y * (t1c[1] + 112 | y * (t1c[2] + 113 | y * (t1c[3] + y * (t1c[4] + y * (t1c[5] + y * t1c[6]))))); 114 | double f1 = 115 | f1c[0] + 116 | y * (f1c[1] + 117 | y * (f1c[2] + 118 | y * (f1c[3] + y * (f1c[4] + y * (f1c[5] + y * f1c[6]))))); 119 | r = sqrt(1.0 / x) * f1 * cos(theta1); 120 | } 121 | return sign * r; 122 | } 123 | 124 | // Evaluates the optical path difference, with derivative d/d_theta in theta, 125 | // the angle between 0 and alpha 126 | __inline void L_theta(complex *L, double theta, parameters p, double ci, 127 | double z, double z_p) 128 | { 129 | double ni2sin2theta = p.ni_2 * sin(theta) * sin(theta); 130 | complex sroot = sqrt(complex(p.ns_2 - ni2sin2theta)); 131 | complex groot = sqrt(complex(p.ng_2 - ni2sin2theta)); 132 | complex g0root = sqrt(complex(p.ng0_2 - ni2sin2theta)); 133 | complex i0root = sqrt(complex(p.ni0_2 - ni2sin2theta)); 134 | L[0] = p.ni * (ci - z) * cos(theta) + z_p * sroot + p.tg * groot - 135 | p.tg0 * g0root - p.ti0 * i0root; 136 | L[1] = p.ni * sin(theta) * 137 | (z - ci + 138 | p.ni * cos(theta) * 139 | (p.tg0 / g0root + p.ti0 / i0root - p.tg / groot - z_p / sroot)); 140 | } 141 | 142 | // Evaluates the optical path difference, together with its partial derivative 143 | // d/d_rho in rho 144 | __inline void L_rho(complex *L, double rho, parameters p, double ci, 145 | double z, double z_p) 146 | { 147 | double NA2rho2 = p.NA * p.NA * rho * rho; 148 | complex iroot = sqrt(complex(p.ni_2 - NA2rho2)); 149 | complex sroot = sqrt(complex(p.ns_2 - NA2rho2)); 150 | complex groot = sqrt(complex(p.ng_2 - NA2rho2)); 151 | complex g0root = sqrt(complex(p.ng0_2 - NA2rho2)); 152 | complex i0root = sqrt(complex(p.ni0_2 - NA2rho2)); 153 | L[0] = (ci - z) * iroot + z_p * sroot + p.tg * groot - p.tg0 * g0root - 154 | p.ti0 * i0root; 155 | L[1] = 2.0 * p.NA * p.NA * rho * 156 | ((z - ci) / iroot - z_p / sroot - p.tg / groot + p.tg0 / g0root + 157 | p.ti0 / i0root); 158 | } 159 | 160 | } // namespace std 161 | #endif // PSFMATH_H 162 | -------------------------------------------------------------------------------- /src/_psfmodels/pythonBindings.cpp: -------------------------------------------------------------------------------- 1 | #include "scalarPSF.cpp" 2 | #include "vectorialPSF.cpp" 3 | #include 4 | #include 5 | 6 | namespace py = pybind11; 7 | 8 | parameters norm_params(float ti0, float ni0, float ni, float tg0, float tg, 9 | float ng0, float ng, float ns, float wvl, float NA, 10 | float dxy, int sf, int mode) 11 | { 12 | parameters p; 13 | 14 | p.ti0 = ti0 * 1e-6; 15 | p.ni0 = ni0; 16 | p.ni = ni; 17 | p.tg0 = tg0 * 1e-6; 18 | p.tg = tg * 1e-6; 19 | p.ng0 = ng0; 20 | p.ng = ng; 21 | p.ns = ns; 22 | p.lambda = wvl * 1e-6; 23 | p.NA = NA; 24 | p.dxy = dxy * 1e-6; 25 | p.sf = sf; 26 | p.mode = mode; 27 | p.k0 = 2 * PI / p.lambda; 28 | p.ni0_2 = p.ni0 * p.ni0; 29 | p.ni_2 = p.ni * p.ni; 30 | p.ng0_2 = p.ng0 * p.ng0; 31 | p.ng_2 = p.ng * p.ng; 32 | p.ns_2 = p.ns * p.ns; 33 | p.alpha = asin(p.NA / p.ni); 34 | p.NA_2 = p.NA * p.NA; 35 | 36 | return p; 37 | } 38 | 39 | py::array_t vectorial_psf(py::array_t zv, int nx, double pz, 40 | double ti0, double ni0, double ni, double tg0, 41 | double tg, double ng0, double ng, double ns, 42 | double wvl, double NA, double dxy, int sf = 3, 43 | int mode = 1) 44 | { 45 | 46 | // convert zv microns to meters 47 | py::buffer_info zvbuf = zv.request(); 48 | double *zvptr = (double *)zvbuf.ptr; 49 | if (zvbuf.ndim != 1) 50 | throw std::runtime_error("zv must be a 1-dimensional array"); 51 | for (py::ssize_t i = 0; i < zv.size(); i++) 52 | { 53 | zvptr[i] *= 1e-6; 54 | } 55 | 56 | double xp[] = {0.0, 0.0, pz * 1e-6}; 57 | 58 | parameters p = 59 | norm_params(ti0, ni0, ni, tg0, tg, ng0, ng, ns, wvl, NA, dxy, sf, mode); 60 | 61 | int nz = zv.shape(0); 62 | VectorialPSF psf = VectorialPSF(xp, zvptr, nz, nx, p); 63 | psf.calculatePSF(); 64 | return py::array_t( 65 | std::vector{nz, nx, nx}, &psf.pixels_[0]); 66 | } 67 | 68 | py::array_t 69 | vectorial_psf_deriv(py::array_t pixdxp, py::array_t pixdyp, 70 | py::array_t pixdzp, py::array_t zv, int nx, 71 | double pz, double ti0, double ni0, double ni, double tg0, 72 | double tg, double ng0, double ng, double ns, double wvl, 73 | double NA, double dxy, int sf = 3, int mode = 1) 74 | { 75 | 76 | // convert zv microns to meters 77 | py::buffer_info zvbuf = zv.request(); 78 | double *zvptr = (double *)zvbuf.ptr; 79 | if (zvbuf.ndim != 1) 80 | throw std::runtime_error("zv must be a 1-dimensional array"); 81 | for (py::ssize_t i = 0; i < zv.size(); i++) 82 | { 83 | zvptr[i] *= 1e-6; 84 | } 85 | 86 | double xp[] = {0.0, 0.0, pz * 1e-6}; 87 | 88 | parameters p = 89 | norm_params(ti0, ni0, ni, tg0, tg, ng0, ng, ns, wvl, NA, dxy, sf, mode); 90 | 91 | int nz = zv.shape(0); 92 | VectorialPSF psf = VectorialPSF(xp, zvptr, nz, nx, p); 93 | psf.calculatePSFdxp(); 94 | 95 | py::buffer_info pixdxpbuf = pixdxp.request(), pixdypbuf = pixdyp.request(), 96 | pixdzpbuf = pixdzp.request(); 97 | double *pixdxpptr = (double *)pixdxpbuf.ptr, 98 | *pixdypptr = (double *)pixdypbuf.ptr, 99 | *pixdzpptr = (double *)pixdzpbuf.ptr; 100 | 101 | for (py::ssize_t idx = 0; idx < pixdxpbuf.size; idx++) 102 | { 103 | pixdxpptr[idx] = psf.pixelsDxp_[idx]; 104 | pixdypptr[idx] = psf.pixelsDyp_[idx]; 105 | pixdzpptr[idx] = psf.pixelsDzp_[idx]; 106 | } 107 | 108 | return py::array_t( 109 | std::vector{nz, nx, nx}, &psf.pixels_[0]); 110 | } 111 | 112 | py::array_t scalar_psf(py::array_t zv, int nx, double pz, 113 | double ti0, double ni0, double ni, double tg0, 114 | double tg, double ng0, double ng, double ns, 115 | double wvl, double NA, double dxy, int sf = 3, 116 | int mode = 1) 117 | { 118 | 119 | // convert zv microns to meters 120 | py::buffer_info zvbuf = zv.request(); 121 | double *zvptr = (double *)zvbuf.ptr; 122 | if (zvbuf.ndim != 1) 123 | throw std::runtime_error("zv must be a 1-dimensional array"); 124 | for (py::ssize_t i = 0; i < zv.size(); i++) 125 | { 126 | zvptr[i] *= 1e-6; 127 | } 128 | 129 | double xp[] = {0.0, 0.0, pz * 1e-6}; 130 | 131 | parameters p = 132 | norm_params(ti0, ni0, ni, tg0, tg, ng0, ng, ns, wvl, NA, dxy, sf, mode); 133 | 134 | int nz = zv.shape(0); 135 | ScalarPSF psf = ScalarPSF(xp, zvptr, nz, nx, p); 136 | psf.calculatePSF(); 137 | return py::array_t( 138 | std::vector{nz, nx, nx}, &psf.pixels_[0]); 139 | } 140 | 141 | PYBIND11_MODULE(_psfmodels, m) 142 | { 143 | m.doc() = "Scalar and Vectorial PSF Models"; // optional module docstring 144 | 145 | m.def("vectorial_psf", &vectorial_psf, R"pbdoc( 146 | Computes a vectorial microscope point spread function model. 147 | 148 | The model is described in F. Aguet et al., Opt. Express 17(8), pp. 6829-6848, 2009 149 | For more information and implementation details, see F. Aguet, Ph.D Thesis, Swiss 150 | Federal Institute of Technology, Lausanne (EPFL), 2009 151 | 152 | C++ code by Francois Aguet, 2009. Python bindings by Talley Lambert, 2019. 153 | 154 | Args: 155 | zv (np.ndarray): Vector of Z positions at which PSF is calculated (in microns, relative to coverslip) 156 | nx (int): XY size of output PSF in pixels, must be odd. 157 | pz (float): point source z position above the coverslip in microns. 158 | ti0 (float): working distance of the objective (microns) 159 | ni0 (float): immersion medium refractive index, design value 160 | ni (float): immersion medium refractive index, experimental value 161 | tg0 (float): coverslip thickness, design value (microns) 162 | tg (float): coverslip thickness, experimental value (microns) 163 | ng0 (float): coverslip refractive index, design value 164 | ng (float): coverslip refractive index, experimental value 165 | ns (float): sample refractive index 166 | wvl (float): emission wavelength (microns) 167 | NA (float): numerical aperture 168 | dxy (float): pixel size in sample space (microns) 169 | sf (int): oversampling factor to approximate pixel integration [default=3] 170 | mode (int): if 0, returns oversampled PSF [default=1] 171 | 172 | Returns: 173 | np.ndarray: PSF with type np.float64 and shape (len(zv), nx, nx) 174 | 175 | )pbdoc", 176 | py::arg("zv"), py::arg("nx"), py::arg("pz"), py::arg("ti0"), 177 | py::arg("ni0"), py::arg("ni"), py::arg("tg0"), py::arg("tg"), 178 | py::arg("ng0"), py::arg("ng"), py::arg("ns"), py::arg("wvl"), 179 | py::arg("NA"), py::arg("dxy"), py::arg("sf") = 3, py::arg("mode") = 1); 180 | 181 | m.def("vectorial_psf_deriv", &vectorial_psf_deriv, R"pbdoc( 182 | Computes a vectorial microscope point spread function model, and returns derivatives. 183 | )pbdoc", 184 | py::arg("pixdxp"), py::arg("pixdyp"), py::arg("pixdzp"), py::arg("zv"), 185 | py::arg("nx"), py::arg("pz"), py::arg("ti0"), py::arg("ni0"), 186 | py::arg("ni"), py::arg("tg0"), py::arg("tg"), py::arg("ng0"), 187 | py::arg("ng"), py::arg("ns"), py::arg("wvl"), py::arg("NA"), 188 | py::arg("dxy"), py::arg("sf") = 3, py::arg("mode") = 1); 189 | 190 | m.def("scalar_psf", &scalar_psf, R"pbdoc( 191 | Computes the scalar PSF model described by Gibson and Lanni 192 | 193 | The model is described in F. Aguet et al., Opt. Express 17(8), pp. 6829-6848, 2009 194 | For more information and implementation details, see F. Aguet, Ph.D Thesis, Swiss 195 | Federal Institute of Technology, Lausanne (EPFL), 2009 196 | 197 | C++ code by Francois Aguet, 2009. Python bindings by Talley Lambert, 2019. 198 | 199 | Args: 200 | zv (np.ndarray): Vector of Z positions at which PSF is calculated (in microns, relative to coverslip) 201 | nx (int): XY size of output PSF in pixels, must be odd. 202 | pz (float): point source z position above the coverslip in microns. 203 | ti0 (float): working distance of the objective (microns) 204 | ni0 (float): immersion medium refractive index, design value 205 | ni (float): immersion medium refractive index, experimental value 206 | tg0 (float): coverslip thickness, design value (microns) 207 | tg (float): coverslip thickness, experimental value (microns) 208 | ng0 (float): coverslip refractive index, design value 209 | ng (float): coverslip refractive index, experimental value 210 | ns (float): sample refractive index 211 | wvl (float): emission wavelength (microns) 212 | NA (float): numerical aperture 213 | dxy (float): pixel size in sample space (microns) 214 | sf (int): oversampling factor to approximate pixel integration [default=3] 215 | mode (int): if 0, returns oversampled PSF [default=1] 216 | 217 | Returns: 218 | np.ndarray: PSF with type np.float64 and shape (len(zv), nx, nx) 219 | 220 | )pbdoc", 221 | py::arg("zv"), py::arg("nx"), py::arg("pz"), py::arg("ti0"), 222 | py::arg("ni0"), py::arg("ni"), py::arg("tg0"), py::arg("tg"), 223 | py::arg("ng0"), py::arg("ng"), py::arg("ns"), py::arg("wvl"), 224 | py::arg("NA"), py::arg("dxy"), py::arg("sf") = 3, py::arg("mode") = 1); 225 | } 226 | -------------------------------------------------------------------------------- /src/_psfmodels/scalarPSF.cpp: -------------------------------------------------------------------------------- 1 | /* scalarPSF.cpp computes the scalar PSF model described by Gibson and Lanni 2 | * [1]. For more information and implementation details, see [2]. 3 | * 4 | * [1] F. Aguet et al., Opt. Express 17(8), pp. 6829-6848, 2009 5 | * [2] F. Aguet, Ph.D Thesis, Swiss Federal Institute of Technology, Lausanne 6 | * (EPFL), 2009 7 | * 8 | * Copyright (C) 2005-2013 Francois Aguet 9 | * 10 | * 11 | * This program is free software: you can redistribute it and/or modify 12 | * it under the terms of the GNU General Public License as published by 13 | * the Free Software Foundation, either version 3 of the License, or 14 | * (at your option) any later version. 15 | * 16 | * This program is distributed in the hope that it will be useful, 17 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | * GNU General Public License for more details. 20 | * 21 | * You should have received a copy of the GNU General Public License 22 | * along with this program. If not, see . 23 | * 24 | * 25 | * MEX compilation: 26 | * Mac/Linux: mex -I../../common/mex/include scalarPSF.cpp 27 | * Windows: mex COMPFLAGS="$COMPFLAGS /MT" -I"..\..\common\mex\include" 28 | * scalarPSF.cpp 29 | */ 30 | 31 | #include "psfmath.h" 32 | #include 33 | 34 | #define NARGIN 4 35 | 36 | using namespace std; 37 | 38 | class ScalarPSF 39 | { 40 | 41 | public: 42 | ScalarPSF(const double xp[], const double z[], const int nz, const int nx, 43 | const parameters p); 44 | ~ScalarPSF(); 45 | 46 | void calculatePSF(); 47 | void calculatePSFdxp(); 48 | 49 | double *pixels_; 50 | double *pixelsDxp_; 51 | double *pixelsDyp_; 52 | double *pixelsDzp_; 53 | 54 | private: 55 | double xp_; 56 | double yp_; 57 | double zp_; 58 | const double *z_; 59 | int nz_; 60 | int nx_; 61 | parameters p_; 62 | 63 | int N_; 64 | double xystep_; 65 | 66 | double **integral_; 67 | double *R; 68 | 69 | int xymax_; 70 | int rmax_; 71 | int npx_; 72 | 73 | static const complex i; 74 | }; 75 | 76 | const complex ScalarPSF::i = complex(0.0, 1.0); 77 | 78 | ScalarPSF::ScalarPSF(const double xp[], const double z[], const int nz, 79 | const int nx, const parameters p) 80 | { 81 | xp_ = xp[0]; 82 | yp_ = xp[1]; 83 | zp_ = xp[2]; 84 | 85 | z_ = z; 86 | nz_ = nz; 87 | nx_ = nx; 88 | p_ = p; 89 | 90 | xystep_ = p.dxy; 91 | 92 | xymax_ = ((nx_)*p.sf - 1) / 2; // always fine scale 93 | if (!p_.mode) 94 | { 95 | nx_ *= p_.sf; // oversampling factor 96 | } 97 | 98 | N_ = nx_ * nx_ * nz_; 99 | 100 | // position in pixels 101 | xp_ *= p.sf / xystep_; 102 | yp_ *= p.sf / xystep_; 103 | 104 | int rn = 1 + (int)sqrt(xp_ * xp_ + yp_ * yp_); 105 | 106 | rmax_ = ceil(sqrt(2.0) * xymax_) + rn + 1; // +1 for interpolation, dx, dy 107 | npx_ = (2 * xymax_ + 1) * (2 * xymax_ + 1); 108 | 109 | pixels_ = new double[N_]; 110 | pixelsDxp_ = new double[N_]; 111 | pixelsDyp_ = new double[N_]; 112 | pixelsDzp_ = new double[N_]; 113 | 114 | integral_ = new double *[nz_]; 115 | for (int k = 0; k < nz_; ++k) 116 | { 117 | integral_[k] = new double[rmax_]; 118 | } 119 | // initialize since loops add to these arrays 120 | memset(pixels_, 0, sizeof(double) * N_); 121 | memset(pixelsDxp_, 0, sizeof(double) * N_); 122 | memset(pixelsDyp_, 0, sizeof(double) * N_); 123 | memset(pixelsDzp_, 0, sizeof(double) * N_); 124 | 125 | // pre-calculate radial coordinates 126 | R = new double[npx_]; 127 | int idx = 0; 128 | double xi, yi; 129 | for (int y = -xymax_; y <= xymax_; ++y) 130 | { 131 | for (int x = -xymax_; x <= xymax_; ++x) 132 | { 133 | xi = (double)x - xp_; 134 | yi = (double)y - yp_; 135 | R[idx] = sqrt(xi * xi + yi * yi); 136 | ++idx; 137 | } 138 | } 139 | } 140 | 141 | ScalarPSF::~ScalarPSF() 142 | { 143 | delete[] R; 144 | for (int k = 0; k < nz_; ++k) 145 | { 146 | delete[] integral_[k]; 147 | } 148 | delete[] integral_; 149 | delete[] pixelsDzp_; 150 | delete[] pixelsDyp_; 151 | delete[] pixelsDxp_; 152 | delete[] pixels_; 153 | } 154 | 155 | void ScalarPSF::calculatePSF() 156 | { 157 | 158 | double r; 159 | int n; 160 | 161 | complex sum_I0, expW; 162 | 163 | // constant component of OPD 164 | double ci = zp_ * (1.0 - p_.ni / p_.ns) + 165 | p_.ni * (p_.tg0 / p_.ng0 + p_.ti0 / p_.ni0 - p_.tg / p_.ng); 166 | 167 | double theta, sintheta, costheta, ni2sin2theta; 168 | double bessel_0; 169 | 170 | double A0 = p_.ni_2 * p_.ni_2 / (p_.NA_2 * p_.NA_2); 171 | 172 | // Integration parameters 173 | double constJ; 174 | int nSamples; 175 | double step; 176 | 177 | double w_exp, cst, iconst; 178 | double ud = 3.0 * p_.sf; 179 | 180 | complex L_th[2]; 181 | for (int k = 0; k < nz_; ++k) 182 | { 183 | 184 | L_theta(L_th, p_.alpha, p_, ci, z_[k], zp_); 185 | w_exp = abs(L_th[1]); // missing p.k0, multiply below 186 | 187 | cst = 0.975; 188 | while (cst >= 0.9) 189 | { 190 | L_theta(L_th, cst * p_.alpha, p_, ci, z_[k], zp_); 191 | if (abs(L_th[1]) > w_exp) 192 | { 193 | w_exp = abs(L_th[1]); 194 | } 195 | cst -= 0.025; 196 | } 197 | w_exp *= p_.k0; 198 | 199 | for (int ri = 0; ri < rmax_; ++ri) 200 | { 201 | r = xystep_ / p_.sf * (double)(ri); 202 | constJ = p_.k0 * r * p_.ni; // samples required for bessel term 203 | 204 | if (w_exp > constJ) 205 | { 206 | nSamples = 4 * (int)(1.0 + p_.alpha * w_exp / PI); 207 | } 208 | else 209 | { 210 | nSamples = 4 * (int)(1.0 + p_.alpha * constJ / PI); 211 | } 212 | if (nSamples < 20) 213 | { 214 | nSamples = 20; 215 | } 216 | step = p_.alpha / (double)nSamples; 217 | iconst = step / ud; 218 | iconst *= iconst; 219 | 220 | // Simpson's rule 221 | sum_I0 = 0.0; 222 | for (n = 1; n < nSamples / 2; n++) 223 | { 224 | theta = 2.0 * n * step; 225 | sintheta = sin(theta); 226 | costheta = cos(theta); 227 | ni2sin2theta = p_.ni_2 * sintheta * sintheta; 228 | bessel_0 = 2.0 * J0(constJ * sintheta) * sintheta * 229 | costheta; // 2.0 factor : Simpson's rule 230 | expW = exp(i * p_.k0 * 231 | ((ci - z_[k]) * p_.ni * costheta + 232 | zp_ * sqrt(complex(p_.ns_2 - ni2sin2theta)) + 233 | p_.tg * sqrt(complex(p_.ng_2 - ni2sin2theta)) - 234 | p_.tg0 * sqrt(complex(p_.ng0_2 - ni2sin2theta)) - 235 | p_.ti0 * sqrt(complex(p_.ni0_2 - ni2sin2theta)))); 236 | sum_I0 += expW * bessel_0; 237 | } 238 | for (n = 1; n <= nSamples / 2; n++) 239 | { 240 | theta = (2.0 * n - 1.0) * step; 241 | sintheta = sin(theta); 242 | costheta = cos(theta); 243 | ni2sin2theta = p_.ni_2 * sintheta * sintheta; 244 | bessel_0 = 4.0 * J0(constJ * sintheta) * sintheta * costheta; 245 | expW = exp(i * p_.k0 * 246 | ((ci - z_[k]) * p_.ni * costheta + 247 | zp_ * sqrt(complex(p_.ns_2 - ni2sin2theta)) + 248 | p_.tg * sqrt(complex(p_.ng_2 - ni2sin2theta)) - 249 | p_.tg0 * sqrt(complex(p_.ng0_2 - ni2sin2theta)) - 250 | p_.ti0 * sqrt(complex(p_.ni0_2 - ni2sin2theta)))); 251 | sum_I0 += expW * bessel_0; 252 | } 253 | // theta = alpha; 254 | bessel_0 = J0(p_.k0 * r * p_.NA) * cos(p_.alpha) * sin(p_.alpha); 255 | expW = exp(i * p_.k0 * 256 | ((ci - z_[k]) * sqrt(complex(p_.ni_2 - p_.NA_2)) + 257 | zp_ * sqrt(complex(p_.ns_2 - p_.NA_2)) + 258 | p_.tg * sqrt(complex(p_.ng_2 - p_.NA_2)) - 259 | p_.tg0 * sqrt(complex(p_.ng0_2 - p_.NA_2)) - 260 | p_.ti0 * sqrt(complex(p_.ni0_2 - p_.NA_2)))); 261 | sum_I0 += expW * bessel_0; 262 | 263 | integral_[k][ri] = A0 * abs(sum_I0) * abs(sum_I0) * iconst; 264 | } 265 | } // z loop 266 | 267 | int k; 268 | double dr; 269 | 270 | // Interpolate (linear) 271 | int r0; 272 | int index = 0; 273 | if (p_.mode == 1) 274 | { // average if sf>1 275 | div_t divRes; 276 | for (k = 0; k < nz_; ++k) 277 | { 278 | for (int i = 0; i < npx_; ++i) 279 | { 280 | r0 = (int)R[i]; 281 | if (r0 + 1 < rmax_) 282 | { 283 | dr = R[i] - r0; 284 | divRes = div(i, 2 * xymax_ + 1); 285 | index = divRes.rem / p_.sf + 286 | (divRes.quot / p_.sf) * nx_; // integer operations! 287 | pixels_[index + k * nx_ * nx_] += 288 | dr * integral_[k][r0 + 1] + (1.0 - dr) * integral_[k][r0]; 289 | } // else '0' 290 | } 291 | } 292 | } 293 | else 294 | { // oversample if sf>1 295 | for (k = 0; k < nz_; ++k) 296 | { 297 | for (int i = 0; i < npx_; ++i) 298 | { 299 | r0 = (int)R[i]; 300 | if (r0 + 1 < rmax_) 301 | { 302 | dr = R[i] - r0; 303 | pixels_[i + k * npx_] = 304 | dr * integral_[k][r0 + 1] + (1.0 - dr) * integral_[k][r0]; 305 | } // else '0' 306 | } 307 | } 308 | } 309 | } 310 | 311 | void ScalarPSF::calculatePSFdxp() 312 | { 313 | 314 | double r; 315 | int n; 316 | 317 | double constJ; 318 | int nSamples; 319 | double step; 320 | 321 | double theta, sintheta, costheta, ni2sin2theta; 322 | complex bessel_0, bessel_1, expW, dW, nsroot; 323 | complex sum_I0, sum_dxI0, sum_dzI0; 324 | complex tmp; 325 | 326 | // allocate dynamic structures 327 | double **integralD; 328 | double **integralDz; 329 | integralD = new double *[nz_]; 330 | integralDz = new double *[nz_]; 331 | for (int k = 0; k < nz_; k++) 332 | { 333 | integralD[k] = new double[rmax_]; 334 | integralDz[k] = new double[rmax_]; 335 | } 336 | 337 | // constant component of OPD 338 | double ci = zp_ * (1.0 - p_.ni / p_.ns) + 339 | p_.ni * (p_.tg0 / p_.ng0 + p_.ti0 / p_.ni0 - p_.tg / p_.ng); 340 | 341 | double A0 = p_.ni_2 * p_.ni_2 / (p_.NA_2 * p_.NA_2); 342 | 343 | int ri; 344 | double ud = 3.0 * p_.sf; 345 | 346 | double w_exp, cst, iconst; 347 | 348 | complex L_th[2]; 349 | 350 | for (int k = 0; k < nz_; ++k) 351 | { 352 | 353 | L_theta(L_th, p_.alpha, p_, ci, z_[k], zp_); 354 | w_exp = abs(L_th[1]); 355 | 356 | cst = 0.975; 357 | while (cst >= 0.9) 358 | { 359 | L_theta(L_th, cst * p_.alpha, p_, ci, z_[k], zp_); 360 | if (abs(L_th[1]) > w_exp) 361 | { 362 | w_exp = abs(L_th[1]); 363 | } 364 | cst -= 0.025; 365 | } 366 | w_exp *= p_.k0; 367 | 368 | for (ri = 0; ri < rmax_; ++ri) 369 | { 370 | 371 | r = xystep_ / p_.sf * (double)(ri); 372 | constJ = p_.k0 * r * p_.ni; 373 | if (w_exp > constJ) 374 | { 375 | nSamples = 4 * (int)(1.0 + p_.alpha * w_exp / PI); 376 | } 377 | else 378 | { 379 | nSamples = 4 * (int)(1.0 + p_.alpha * constJ / PI); 380 | } 381 | if (nSamples < 20) 382 | { 383 | nSamples = 20; 384 | } 385 | step = p_.alpha / (double)nSamples; 386 | iconst = step / ud; 387 | iconst *= iconst; 388 | 389 | // Simpson's rule 390 | sum_I0 = 0.0; 391 | sum_dxI0 = 0.0; 392 | sum_dzI0 = 0.0; 393 | 394 | for (n = 1; n < nSamples / 2; n++) 395 | { 396 | theta = 2.0 * n * step; 397 | sintheta = sin(theta); 398 | costheta = cos(theta); 399 | ni2sin2theta = p_.ni_2 * sintheta * sintheta; 400 | nsroot = sqrt(complex(p_.ns_2 - ni2sin2theta)); 401 | 402 | bessel_0 = 2.0 * J0(constJ * sintheta) * sintheta * 403 | costheta; // 2.0 factor : Simpson's rule 404 | bessel_1 = 2.0 * J1(constJ * sintheta) * sintheta * costheta; 405 | 406 | expW = exp(i * p_.k0 * 407 | ((ci - z_[k]) * p_.ni * costheta + zp_ * nsroot + 408 | p_.tg * sqrt(complex(p_.ng_2 - ni2sin2theta)) - 409 | p_.tg0 * sqrt(complex(p_.ng0_2 - ni2sin2theta)) - 410 | p_.ti0 * sqrt(complex(p_.ni0_2 - ni2sin2theta)))); 411 | dW = i * ((1.0 - p_.ni / p_.ns) * p_.ni * costheta + nsroot); 412 | 413 | tmp = expW * bessel_0; 414 | sum_I0 += tmp; 415 | tmp *= dW; 416 | sum_dzI0 += tmp; 417 | sum_dxI0 += expW * bessel_1 * sintheta; 418 | } 419 | for (n = 1; n <= nSamples / 2; n++) 420 | { 421 | theta = (2.0 * n - 1.0) * step; 422 | sintheta = sin(theta); 423 | costheta = cos(theta); 424 | ni2sin2theta = p_.ni_2 * sintheta * sintheta; 425 | nsroot = sqrt(complex(p_.ns_2 - ni2sin2theta)); 426 | 427 | bessel_0 = 4.0 * J0(constJ * sintheta) * sintheta * 428 | costheta; // 4.0 factor : Simpson's rule 429 | bessel_1 = 4.0 * J1(constJ * sintheta) * sintheta * costheta; 430 | 431 | expW = exp(i * p_.k0 * 432 | ((ci - z_[k]) * p_.ni * costheta + zp_ * nsroot + 433 | p_.tg * sqrt(complex(p_.ng_2 - ni2sin2theta)) - 434 | p_.tg0 * sqrt(complex(p_.ng0_2 - ni2sin2theta)) - 435 | p_.ti0 * sqrt(complex(p_.ni0_2 - ni2sin2theta)))); 436 | dW = i * ((1.0 - p_.ni / p_.ns) * p_.ni * costheta + nsroot); 437 | 438 | tmp = expW * bessel_0; 439 | sum_I0 += tmp; 440 | tmp *= dW; 441 | sum_dzI0 += tmp; 442 | sum_dxI0 += expW * bessel_1 * sintheta; 443 | } 444 | // theta = alpha; 445 | sintheta = sin(p_.alpha); 446 | costheta = cos(p_.alpha); 447 | nsroot = sqrt(complex(p_.ns_2 - p_.NA_2)); 448 | 449 | bessel_0 = J0(constJ * sintheta) * sintheta * costheta; 450 | bessel_1 = J1(constJ * sintheta) * sintheta * costheta; 451 | 452 | expW = exp(i * p_.k0 * 453 | ((ci - z_[k]) * p_.ni * costheta + zp_ * nsroot + 454 | p_.tg * sqrt(complex(p_.ng_2 - p_.NA_2)) - 455 | p_.tg0 * sqrt(complex(p_.ng0_2 - p_.NA_2)) - 456 | p_.ti0 * sqrt(complex(p_.ni0_2 - p_.NA_2)))); 457 | dW = i * ((1.0 - p_.ni / p_.ns) * p_.ni * costheta + nsroot); 458 | 459 | tmp = expW * bessel_0; 460 | sum_I0 += tmp; 461 | tmp *= dW; 462 | sum_dzI0 += tmp; 463 | sum_dxI0 += expW * bessel_1 * sintheta; 464 | 465 | integral_[k][ri] = A0 * abs(sum_I0) * abs(sum_I0) * iconst; 466 | integralD[k][ri] = p_.k0 * p_.ni * A0 / r * 2.0 * 467 | real(conj(sum_I0) * sum_dxI0) * 468 | iconst; // multiply with (x-xp) 469 | integralDz[k][ri] = 470 | p_.k0 * A0 * 2.0 * real(conj(sum_I0) * sum_dzI0) * iconst; 471 | } 472 | integralD[k][0] = 0.0; // overwrite because of singularity 473 | } // z loop 474 | 475 | // Interpolate (linear) 476 | int r0; 477 | double dr, rx; 478 | double xi, yi, tmp2; 479 | int index = 0; 480 | int k, x, y; 481 | if (p_.mode == 1) 482 | { 483 | for (k = 0; k < nz_; k++) 484 | { 485 | for (y = -xymax_; y <= xymax_; y++) 486 | { 487 | for (x = -xymax_; x <= xymax_; x++) 488 | { 489 | 490 | xi = (double)x - xp_; 491 | yi = (double)y - yp_; 492 | rx = sqrt(xi * xi + yi * yi); 493 | r0 = (int)rx; 494 | 495 | if (r0 + 1 < rmax_) 496 | { 497 | dr = rx - r0; 498 | index = (x + xymax_) / p_.sf + ((y + xymax_) / p_.sf) * nx_ + 499 | k * nx_ * nx_; 500 | 501 | pixels_[index] += 502 | dr * integral_[k][r0 + 1] + (1.0 - dr) * integral_[k][r0]; 503 | pixelsDzp_[index] += 504 | dr * integralDz[k][r0 + 1] + (1.0 - dr) * integralDz[k][r0]; 505 | 506 | xi *= xystep_ / p_.sf; 507 | yi *= xystep_ / p_.sf; 508 | 509 | tmp2 = dr * integralD[k][r0 + 1] + (1.0 - dr) * integralD[k][r0]; 510 | pixelsDxp_[index] += xi * tmp2; 511 | pixelsDyp_[index] += yi * tmp2; 512 | } // else '0' 513 | } 514 | } 515 | } 516 | } 517 | else 518 | { 519 | for (k = 0; k < nz_; k++) 520 | { 521 | for (y = -xymax_; y <= xymax_; y++) 522 | { 523 | for (x = -xymax_; x <= xymax_; x++) 524 | { 525 | 526 | xi = (double)x - xp_; 527 | yi = (double)y - yp_; 528 | rx = sqrt(xi * xi + yi * yi); 529 | r0 = (int)rx; 530 | 531 | if (r0 + 1 < rmax_) 532 | { 533 | dr = rx - r0; 534 | pixels_[index] += 535 | dr * integral_[k][r0 + 1] + (1.0 - dr) * integral_[k][r0]; 536 | pixelsDzp_[index] += 537 | dr * integralDz[k][r0 + 1] + (1.0 - dr) * integralDz[k][r0]; 538 | 539 | xi *= xystep_ / p_.sf; 540 | yi *= xystep_ / p_.sf; 541 | 542 | tmp2 = dr * integralD[k][r0 + 1] + (1.0 - dr) * integralD[k][r0]; 543 | pixelsDxp_[index] += xi * tmp2; 544 | pixelsDyp_[index] += yi * tmp2; 545 | } // else '0' 546 | index++; 547 | } 548 | } 549 | } 550 | } 551 | 552 | delete[] integralDz; 553 | delete[] integralD; 554 | } 555 | 556 | // compiled with: 557 | // export DYLD_LIBRARY_PATH=/Applications/MATLAB_R2013a.app/bin/maci64 && g++ 558 | // -Wall -g -DARRAY_ACCESS_INLINING -I. 559 | // -L/Applications/MATLAB_R2013a.app/bin/maci64 -I../../mex/include/ 560 | // -I/Applications/MATLAB_R2013a.app/extern/include scalarPSF.cpp -lmx -lmex 561 | // tested with: 562 | // valgrind --tool=memcheck --leak-check=full --show-reachable=yes ./a.out 2>&1 563 | // | grep scalarPSF 564 | 565 | // int main(void) { 566 | // double xp[] = {0.0, 0.0, 0.0}; 567 | // double z[] = {-100e-9, 0, 100e-9}; 568 | // int nx = 31; 569 | // int nz = 3; 570 | // 571 | // parameters p; 572 | // 573 | // p.ti0 = 1.9e-4; 574 | // p.ni0 = 1.518; 575 | // p.ni0_2 = p.ni0*p.ni0; 576 | // p.ni = 1.518; 577 | // p.ni_2 = p.ni*p.ni; 578 | // p.tg0 = 1.7e-4; 579 | // p.tg = 1.7e-4; 580 | // p.ng0 = 1.515; 581 | // p.ng0_2 = p.ng0*p.ng0; 582 | // p.ng = 1.515; 583 | // p.ng_2 = p.ng*p.ng; 584 | // p.ns = 1.33; 585 | // p.ns_2 = p.ns*p.ns; 586 | // p.lambda = 550e-9; 587 | // p.k0 = 2*PI/p.lambda; 588 | // p.M = 100; 589 | // p.NA = 1.45; 590 | // p.NA_2 = p.NA*p.NA; 591 | // p.alpha = asin(p.NA/p.ni); 592 | // p.pixelSize = 6.45e-6; 593 | // p.sf = 3; 594 | // p.mode = 1; 595 | // 596 | // ScalarPSF psf = ScalarPSF(xp, z, nz, nx, p); 597 | // psf.calculatePSF(); 598 | // psf.calculatePSFdxp(); 599 | // printf("Done.\n"); 600 | // } 601 | -------------------------------------------------------------------------------- /src/_psfmodels/vectorialPSF.cpp: -------------------------------------------------------------------------------- 1 | /* vectorialPSF.cpp computes a vectorial model of the microscope point spread 2 | * function [1]. For more information and implementation details, see [2]. 3 | * 4 | * [1] F. Aguet et al., Opt. Express 17(8), pp. 6829-6848, 2009 5 | * [2] F. Aguet, Ph.D Thesis, Swiss Federal Institute of Technology, Lausanne 6 | * (EPFL), 2009 7 | * 8 | * Copyright (C) 2006-2013 Francois Aguet 9 | * 10 | * 11 | * This program is free software: you can redistribute it and/or modify 12 | * it under the terms of the GNU General Public License as published by 13 | * the Free Software Foundation, either version 3 of the License, or 14 | * (at your option) any later version. 15 | * 16 | * This program is distributed in the hope that it will be useful, 17 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | * GNU General Public License for more details. 20 | * 21 | * You should have received a copy of the GNU General Public License 22 | * along with this program. If not, see . 23 | * 24 | * 25 | * MEX compilation: 26 | * Mac/Linux: mex -I../../mex/include vectorialPSF.cpp 27 | * Windows: mex COMPFLAGS="$COMPFLAGS /MT" -I"..\..\mex\include" 28 | * vectorialPSF.cpp 29 | */ 30 | 31 | #include "psfmath.h" 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | 38 | #define NARGIN 4 39 | 40 | using namespace std; 41 | 42 | template 43 | std::vector linspace(T a, T b, size_t N) 44 | { 45 | T h = (b - a) / static_cast(N - 1); 46 | std::vector xs(N); 47 | typename std::vector::iterator x; 48 | T val; 49 | for (x = xs.begin(), val = a; x != xs.end(); ++x, val += h) 50 | *x = val; 51 | return xs; 52 | } 53 | 54 | class VectorialPSF 55 | { 56 | 57 | public: 58 | VectorialPSF(const double xp[], const double z[], const int nz, const int nx, 59 | const parameters p); 60 | ~VectorialPSF(); 61 | 62 | void calculatePSF(); 63 | void calculatePSFdxp(); 64 | 65 | double *pixels_; 66 | double *pixelsDxp_; 67 | double *pixelsDyp_; 68 | double *pixelsDzp_; 69 | 70 | private: 71 | double xp_; 72 | double yp_; 73 | double zp_; 74 | const double *z_; 75 | int nz_; 76 | int nx_; 77 | parameters p_; 78 | 79 | int N_; 80 | double xystep_; 81 | 82 | double **integral_; 83 | double *R; 84 | 85 | int xymax_; 86 | int rmax_; 87 | int npx_; 88 | 89 | static const complex i; 90 | }; 91 | 92 | const complex VectorialPSF::i = complex(0.0, 1.0); 93 | 94 | VectorialPSF::VectorialPSF(const double xp[], const double z[], const int nz, 95 | const int nx, const parameters p) 96 | { 97 | xp_ = xp[0]; 98 | yp_ = xp[1]; 99 | zp_ = xp[2]; 100 | 101 | z_ = z; 102 | nz_ = nz; 103 | nx_ = nx; 104 | p_ = p; 105 | 106 | xystep_ = p.dxy; 107 | 108 | xymax_ = ((nx_)*p.sf - 1) / 2; // always fine scale 109 | if (!p_.mode) 110 | { 111 | nx_ *= p_.sf; // oversampling factor 112 | } 113 | 114 | N_ = nx_ * nx_ * nz_; 115 | 116 | // position in pixels 117 | xp_ *= p.sf / xystep_; 118 | yp_ *= p.sf / xystep_; 119 | 120 | int rn = 1 + (int)sqrt(xp_ * xp_ + yp_ * yp_); 121 | 122 | rmax_ = ceil(sqrt(2.0) * xymax_) + rn + 1; // +1 for interpolation, dx, dy 123 | npx_ = (2 * xymax_ + 1) * (2 * xymax_ + 1); 124 | 125 | pixels_ = new double[N_]; 126 | pixelsDxp_ = new double[N_]; 127 | pixelsDyp_ = new double[N_]; 128 | pixelsDzp_ = new double[N_]; 129 | 130 | integral_ = new double *[nz_]; 131 | for (int k = 0; k < nz_; ++k) 132 | { 133 | integral_[k] = new double[rmax_]; 134 | } 135 | // initialize since loops add to these arrays 136 | memset(pixels_, 0, sizeof(double) * N_); 137 | memset(pixelsDxp_, 0, sizeof(double) * N_); 138 | memset(pixelsDyp_, 0, sizeof(double) * N_); 139 | memset(pixelsDzp_, 0, sizeof(double) * N_); 140 | 141 | // pre-calculate radial coordinates 142 | R = new double[npx_]; 143 | int idx = 0; 144 | double xi, yi; 145 | for (int y = -xymax_; y <= xymax_; ++y) 146 | { 147 | for (int x = -xymax_; x <= xymax_; ++x) 148 | { 149 | xi = (double)x - xp_; 150 | yi = (double)y - yp_; 151 | R[idx] = sqrt(xi * xi + yi * yi); 152 | ++idx; 153 | } 154 | } 155 | } 156 | 157 | VectorialPSF::~VectorialPSF() 158 | { 159 | delete[] R; 160 | for (int k = 0; k < nz_; ++k) 161 | { 162 | delete[] integral_[k]; 163 | } 164 | delete[] integral_; 165 | delete[] pixelsDzp_; 166 | delete[] pixelsDyp_; 167 | delete[] pixelsDxp_; 168 | delete[] pixels_; 169 | } 170 | 171 | // Intensity PSF for an isotropically emitting point source (average of all 172 | // dipole orientations) 173 | void VectorialPSF::calculatePSF() 174 | { 175 | 176 | double r; 177 | int n; 178 | 179 | // Integration parameters 180 | double constJ; 181 | int nSamples; 182 | double step; 183 | 184 | double theta, sintheta, costheta, sqrtcostheta, ni2sin2theta; 185 | complex bessel_0, bessel_1, bessel_2, expW; 186 | complex ngroot, nsroot; 187 | complex ts1ts2, tp1tp2; 188 | complex sum_I0, sum_I1, sum_I2; 189 | 190 | // constant component of OPD 191 | double ci = zp_ * (1.0 - p_.ni / p_.ns) + 192 | p_.ni * (p_.tg0 / p_.ng0 + p_.ti0 / p_.ni0 - p_.tg / p_.ng); 193 | 194 | int x, y, index, ri; 195 | double iconst; 196 | double ud = 3.0 * p_.sf; 197 | 198 | double w_exp; 199 | 200 | complex L_th[2]; 201 | double cst; 202 | 203 | for (int k = 0; k < nz_; k++) 204 | { 205 | 206 | L_theta(L_th, p_.alpha, p_, ci, z_[k], zp_); 207 | w_exp = abs(L_th[1]); // missing p.k0, multiply below 208 | 209 | cst = 0.975; 210 | while (cst >= 0.9) 211 | { 212 | L_theta(L_th, cst * p_.alpha, p_, ci, z_[k], zp_); 213 | if (abs(L_th[1]) > w_exp) 214 | { 215 | w_exp = abs(L_th[1]); 216 | } 217 | cst -= 0.025; 218 | } 219 | w_exp *= p_.k0; 220 | 221 | for (ri = 0; ri < rmax_; ++ri) 222 | { 223 | 224 | r = xystep_ / p_.sf * (double)(ri); 225 | constJ = p_.k0 * r * p_.ni; // = w_J; 226 | 227 | if (w_exp > constJ) 228 | { 229 | nSamples = 4 * (int)(1.0 + p_.alpha * w_exp / PI); 230 | } 231 | else 232 | { 233 | nSamples = 4 * (int)(1.0 + p_.alpha * constJ / PI); 234 | } 235 | if (nSamples < 20) 236 | { 237 | nSamples = 20; 238 | } 239 | 240 | step = p_.alpha / (double)nSamples; 241 | iconst = step / ud; 242 | 243 | // Simpson's rule 244 | sum_I0 = 0.0; 245 | sum_I1 = 0.0; 246 | sum_I2 = 0.0; 247 | 248 | for (n = 1; n < nSamples / 2; n++) 249 | { 250 | theta = 2.0 * n * step; 251 | sintheta = sin(theta); 252 | costheta = cos(theta); 253 | sqrtcostheta = sqrt(costheta); 254 | ni2sin2theta = p_.ni_2 * sintheta * sintheta; 255 | nsroot = sqrt(complex(p_.ns_2 - ni2sin2theta)); 256 | ngroot = sqrt(complex(p_.ng_2 - ni2sin2theta)); 257 | 258 | ts1ts2 = 4.0 * p_.ni * costheta * ngroot; 259 | tp1tp2 = ts1ts2; 260 | tp1tp2 /= (p_.ng * costheta + p_.ni / p_.ng * ngroot) * 261 | (p_.ns / p_.ng * ngroot + p_.ng / p_.ns * nsroot); 262 | ts1ts2 /= (p_.ni * costheta + ngroot) * (ngroot + nsroot); 263 | 264 | bessel_0 = 2.0 * J0(constJ * sintheta) * sintheta * 265 | sqrtcostheta; // 2.0 factor : Simpson's rule 266 | bessel_1 = 2.0 * J1(constJ * sintheta) * sintheta * sqrtcostheta; 267 | if (constJ != 0.0) 268 | { 269 | bessel_2 = 2.0 * bessel_1 / (constJ * sintheta) - bessel_0; 270 | } 271 | else 272 | { 273 | bessel_2 = 0.0; 274 | } 275 | bessel_0 *= (ts1ts2 + tp1tp2 / p_.ns * nsroot); 276 | bessel_1 *= (tp1tp2 * p_.ni / p_.ns * sintheta); 277 | bessel_2 *= (ts1ts2 - tp1tp2 / p_.ns * nsroot); 278 | 279 | expW = exp(i * p_.k0 * 280 | ((ci - z_[k]) * p_.ni * costheta + zp_ * nsroot + 281 | p_.tg * ngroot - 282 | p_.tg0 * sqrt(complex(p_.ng0_2 - ni2sin2theta)) - 283 | p_.ti0 * sqrt(complex(p_.ni0_2 - ni2sin2theta)))); 284 | sum_I0 += expW * bessel_0; 285 | sum_I1 += expW * bessel_1; 286 | sum_I2 += expW * bessel_2; 287 | } 288 | for (n = 1; n <= nSamples / 2; n++) 289 | { 290 | theta = (2.0 * n - 1) * step; 291 | sintheta = sin(theta); 292 | costheta = cos(theta); 293 | sqrtcostheta = sqrt(costheta); 294 | ni2sin2theta = p_.ni_2 * sintheta * sintheta; 295 | nsroot = sqrt(complex(p_.ns_2 - ni2sin2theta)); 296 | ngroot = sqrt(complex(p_.ng_2 - ni2sin2theta)); 297 | 298 | ts1ts2 = 4.0 * p_.ni * costheta * ngroot; 299 | tp1tp2 = ts1ts2; 300 | tp1tp2 /= (p_.ng * costheta + p_.ni / p_.ng * ngroot) * 301 | (p_.ns / p_.ng * ngroot + p_.ng / p_.ns * nsroot); 302 | ts1ts2 /= (p_.ni * costheta + ngroot) * (ngroot + nsroot); 303 | 304 | bessel_0 = 4.0 * J0(constJ * sintheta) * sintheta * 305 | sqrtcostheta; // 4.0 factor : Simpson's rule 306 | bessel_1 = 4.0 * J1(constJ * sintheta) * sintheta * sqrtcostheta; 307 | if (constJ != 0.0) 308 | { 309 | bessel_2 = 2.0 * bessel_1 / (constJ * sintheta) - bessel_0; 310 | } 311 | else 312 | { 313 | bessel_2 = 0.0; 314 | } 315 | bessel_0 *= (ts1ts2 + tp1tp2 / p_.ns * nsroot); 316 | bessel_1 *= (tp1tp2 * p_.ni / p_.ns * sintheta); 317 | bessel_2 *= (ts1ts2 - tp1tp2 / p_.ns * nsroot); 318 | 319 | expW = exp(i * p_.k0 * 320 | ((ci - z_[k]) * p_.ni * costheta + zp_ * nsroot + 321 | p_.tg * ngroot - 322 | p_.tg0 * sqrt(complex(p_.ng0_2 - ni2sin2theta)) - 323 | p_.ti0 * sqrt(complex(p_.ni0_2 - ni2sin2theta)))); 324 | sum_I0 += expW * bessel_0; 325 | sum_I1 += expW * bessel_1; 326 | sum_I2 += expW * bessel_2; 327 | } 328 | // theta = alpha; 329 | sintheta = sin(p_.alpha); 330 | costheta = cos(p_.alpha); 331 | sqrtcostheta = sqrt(costheta); 332 | nsroot = sqrt(complex(p_.ns_2 - p_.NA_2)); 333 | ngroot = sqrt(complex(p_.ng_2 - p_.NA_2)); 334 | 335 | ts1ts2 = 4.0 * p_.ni * costheta * ngroot; 336 | tp1tp2 = ts1ts2; 337 | tp1tp2 /= (p_.ng * costheta + p_.ni / p_.ng * ngroot) * 338 | (p_.ns / p_.ng * ngroot + p_.ng / p_.ns * nsroot); 339 | ts1ts2 /= (p_.ni * costheta + ngroot) * (ngroot + nsroot); 340 | 341 | bessel_0 = J0(constJ * sintheta) * sintheta * sqrtcostheta; 342 | bessel_1 = J1(constJ * sintheta) * sintheta * sqrtcostheta; 343 | if (constJ != 0.0) 344 | { 345 | bessel_2 = 2.0 * bessel_1 / (constJ * sintheta) - bessel_0; 346 | } 347 | else 348 | { 349 | bessel_2 = 0.0; 350 | } 351 | bessel_0 *= (ts1ts2 + tp1tp2 / p_.ns * nsroot); 352 | bessel_1 *= (tp1tp2 * p_.ni / p_.ns * sintheta); 353 | bessel_2 *= (ts1ts2 - tp1tp2 / p_.ns * nsroot); 354 | 355 | expW = exp(i * p_.k0 * 356 | ((ci - z_[k]) * sqrt(complex(p_.ni_2 - p_.NA_2)) + 357 | zp_ * nsroot + p_.tg * ngroot - 358 | p_.tg0 * sqrt(complex(p_.ng0_2 - p_.NA_2)) - 359 | p_.ti0 * sqrt(complex(p_.ni0_2 - p_.NA_2)))); 360 | sum_I0 += expW * bessel_0; 361 | sum_I1 += expW * bessel_1; 362 | sum_I2 += expW * bessel_2; 363 | 364 | sum_I0 = abs(sum_I0); 365 | sum_I1 = abs(sum_I1); 366 | sum_I2 = abs(sum_I2); 367 | 368 | integral_[k][ri] = 369 | 8.0 * PI / 3.0 * 370 | real(sum_I0 * sum_I0 + 2.0 * sum_I1 * sum_I1 + sum_I2 * sum_I2) * 371 | iconst * iconst; 372 | } 373 | } // z loop 374 | 375 | // Interpolate (linear) 376 | int r0; 377 | double dr, rx, xi, yi; 378 | index = 0; 379 | if (p_.mode == 1) 380 | { 381 | for (int k = 0; k < nz_; ++k) 382 | { 383 | for (y = -xymax_; y <= xymax_; y++) 384 | { 385 | for (x = -xymax_; x <= xymax_; x++) 386 | { 387 | xi = (double)x - xp_; 388 | yi = (double)y - yp_; 389 | rx = sqrt(xi * xi + yi * yi); 390 | r0 = (int)rx; 391 | if (r0 + 1 < rmax_) 392 | { 393 | dr = rx - r0; 394 | index = (x + xymax_) / p_.sf + ((y + xymax_) / p_.sf) * nx_ + 395 | k * nx_ * nx_; 396 | pixels_[index] += 397 | dr * integral_[k][r0 + 1] + (1.0 - dr) * integral_[k][r0]; 398 | } // else '0' 399 | } 400 | } 401 | } 402 | } 403 | else 404 | { 405 | for (int k = 0; k < nz_; ++k) 406 | { 407 | for (y = -xymax_; y <= xymax_; y++) 408 | { 409 | for (x = -xymax_; x <= xymax_; x++) 410 | { 411 | xi = (double)x - xp_; 412 | yi = (double)y - yp_; 413 | rx = sqrt(xi * xi + yi * yi); 414 | r0 = (int)rx; 415 | if (r0 + 1 < rmax_) 416 | { 417 | dr = rx - r0; 418 | pixels_[index] += 419 | dr * integral_[k][r0 + 1] + (1.0 - dr) * integral_[k][r0]; 420 | } // else '0' 421 | index++; 422 | } 423 | } 424 | } 425 | } 426 | } // psf 427 | 428 | // Same PSF calculation as above, but including partial derivatives relative to 429 | // source pos. xp 430 | void VectorialPSF::calculatePSFdxp() 431 | { 432 | 433 | double r; 434 | int n; 435 | 436 | // Integration parameters 437 | double constJ; 438 | int nSamples; 439 | double step; 440 | 441 | double theta, sintheta, costheta, sqrtcostheta, ni2sin2theta; 442 | complex bessel_0, bessel_1, bessel_2, bessel_3; 443 | complex ngroot, nsroot; 444 | complex ts1ts2, tp1tp2; 445 | complex sum_I0, sum_I1, sum_I2, sum_dxI0, sum_dxI1, sum_dxI2, 446 | sum_dzI0, sum_dzI1, sum_dzI2; 447 | complex t0, t1, t2; 448 | complex expW, dW, tmp; 449 | 450 | double xystep = p_.dxy; 451 | 452 | // constant component of OPD 453 | double ci = zp_ * (1.0 - p_.ni / p_.ns) + 454 | p_.ni * (p_.tg0 / p_.ng0 + p_.ti0 / p_.ni0 - p_.tg / p_.ng); 455 | 456 | // allocate dynamic structures 457 | double **integralDx; 458 | double **integralDz; 459 | integralDx = new double *[nz_]; 460 | integralDz = new double *[nz_]; 461 | for (int k = 0; k < nz_; ++k) 462 | { 463 | integralDx[k] = new double[rmax_]; 464 | integralDz[k] = new double[rmax_]; 465 | } 466 | 467 | int x, y, index, ri; 468 | double iconst; 469 | double ud = 3.0 * p_.sf; 470 | 471 | double w_exp; 472 | 473 | complex L_th[2]; 474 | double cst; 475 | 476 | for (int k = 0; k < nz_; ++k) 477 | { 478 | 479 | L_theta(L_th, p_.alpha, p_, ci, z_[k], zp_); 480 | w_exp = abs(L_th[1]); // missing p.k0 ! 481 | 482 | cst = 0.975; 483 | while (cst >= 0.9) 484 | { 485 | L_theta(L_th, cst * p_.alpha, p_, ci, z_[k], zp_); 486 | if (abs(L_th[1]) > w_exp) 487 | { 488 | w_exp = abs(L_th[1]); 489 | } 490 | cst -= 0.025; 491 | } 492 | w_exp *= p_.k0; 493 | 494 | for (ri = 0; ri < rmax_; ++ri) 495 | { 496 | 497 | r = xystep / p_.sf * (double)(ri); 498 | constJ = p_.k0 * r * p_.ni; // = w_J; 499 | 500 | if (w_exp > constJ) 501 | { 502 | nSamples = 4 * (int)(1.0 + p_.alpha * w_exp / PI); 503 | } 504 | else 505 | { 506 | nSamples = 4 * (int)(1.0 + p_.alpha * constJ / PI); 507 | } 508 | if (nSamples < 20) 509 | { 510 | nSamples = 20; 511 | } 512 | step = p_.alpha / (double)nSamples; 513 | iconst = step / ud; 514 | 515 | // Simpson's rule 516 | sum_I0 = 0.0; 517 | sum_I1 = 0.0; 518 | sum_I2 = 0.0; 519 | sum_dxI0 = 0.0; 520 | sum_dxI1 = 0.0; 521 | sum_dxI2 = 0.0; 522 | sum_dzI0 = 0.0; 523 | sum_dzI1 = 0.0; 524 | sum_dzI2 = 0.0; 525 | 526 | for (n = 1; n < nSamples / 2; n++) 527 | { 528 | theta = 2.0 * n * step; 529 | sintheta = sin(theta); 530 | costheta = cos(theta); 531 | sqrtcostheta = sqrt(costheta); 532 | ni2sin2theta = p_.ni_2 * sintheta * sintheta; 533 | nsroot = sqrt(complex(p_.ns_2 - ni2sin2theta)); 534 | ngroot = sqrt(complex(p_.ng_2 - ni2sin2theta)); 535 | 536 | ts1ts2 = 4.0 * p_.ni * costheta * ngroot; 537 | tp1tp2 = ts1ts2; 538 | tp1tp2 /= (p_.ng * costheta + p_.ni / p_.ng * ngroot) * 539 | (p_.ns / p_.ng * ngroot + p_.ng / p_.ns * nsroot); 540 | ts1ts2 /= (p_.ni * costheta + ngroot) * (ngroot + nsroot); 541 | 542 | bessel_0 = 2.0 * J0(constJ * sintheta) * sintheta * 543 | sqrtcostheta; // 2.0 factor : Simpson's rule 544 | bessel_1 = 2.0 * J1(constJ * sintheta) * sintheta * sqrtcostheta; 545 | if (constJ != 0.0) 546 | { 547 | bessel_2 = 2.0 * bessel_1 / (constJ * sintheta) - bessel_0; 548 | bessel_3 = 4.0 * bessel_2 / (constJ * sintheta) - bessel_1; 549 | } 550 | else 551 | { 552 | bessel_2 = 0.0; 553 | bessel_3 = 0.0; 554 | } 555 | 556 | t0 = ts1ts2 + tp1tp2 / p_.ns * nsroot; 557 | t1 = tp1tp2 * p_.ni / p_.ns * sintheta; 558 | t2 = ts1ts2 - tp1tp2 / p_.ns * nsroot; 559 | 560 | expW = exp(i * p_.k0 * 561 | ((ci - z_[k]) * p_.ni * costheta + zp_ * nsroot + 562 | p_.tg * ngroot - 563 | p_.tg0 * sqrt(complex(p_.ng0_2 - ni2sin2theta)) - 564 | p_.ti0 * sqrt(complex(p_.ni0_2 - ni2sin2theta)))); 565 | dW = i * ((1.0 - p_.ni / p_.ns) * p_.ni * costheta + nsroot); 566 | 567 | tmp = expW * bessel_0 * t0; 568 | sum_I0 += tmp; 569 | sum_dzI0 += tmp * dW; 570 | tmp = expW * bessel_1 * t1; 571 | sum_I1 += tmp; 572 | sum_dzI1 += tmp * dW; 573 | tmp = expW * bessel_2 * t2; 574 | sum_I2 += tmp; 575 | sum_dzI2 += tmp * dW; 576 | 577 | sum_dxI0 += expW * bessel_1 * t0 * sintheta; 578 | sum_dxI1 += expW * (bessel_0 - bessel_2) * t1 * sintheta; 579 | sum_dxI2 += expW * (bessel_1 - bessel_3) * t2 * sintheta; 580 | } 581 | for (n = 1; n <= nSamples / 2; n++) 582 | { 583 | theta = (2.0 * n - 1) * step; 584 | sintheta = sin(theta); 585 | costheta = cos(theta); 586 | sqrtcostheta = sqrt(costheta); 587 | ni2sin2theta = p_.ni_2 * sintheta * sintheta; 588 | nsroot = sqrt(complex(p_.ns_2 - ni2sin2theta)); 589 | ngroot = sqrt(complex(p_.ng_2 - ni2sin2theta)); 590 | 591 | ts1ts2 = 4.0 * p_.ni * costheta * ngroot; 592 | tp1tp2 = ts1ts2; 593 | tp1tp2 /= (p_.ng * costheta + p_.ni / p_.ng * ngroot) * 594 | (p_.ns / p_.ng * ngroot + p_.ng / p_.ns * nsroot); 595 | ts1ts2 /= (p_.ni * costheta + ngroot) * (ngroot + nsroot); 596 | 597 | bessel_0 = 4.0 * J0(constJ * sintheta) * sintheta * 598 | sqrtcostheta; // 4.0 factor : Simpson's rule 599 | bessel_1 = 4.0 * J1(constJ * sintheta) * sintheta * sqrtcostheta; 600 | if (constJ != 0.0) 601 | { 602 | bessel_2 = 2.0 * bessel_1 / (constJ * sintheta) - bessel_0; 603 | bessel_3 = 4.0 * bessel_2 / (constJ * sintheta) - bessel_1; 604 | } 605 | else 606 | { 607 | bessel_2 = 0.0; 608 | bessel_3 = 0.0; 609 | } 610 | t0 = ts1ts2 + tp1tp2 / p_.ns * nsroot; 611 | t1 = tp1tp2 * p_.ni / p_.ns * sintheta; 612 | t2 = ts1ts2 - tp1tp2 / p_.ns * nsroot; 613 | 614 | expW = exp(i * p_.k0 * 615 | ((ci - z_[k]) * p_.ni * costheta + zp_ * nsroot + 616 | p_.tg * ngroot - 617 | p_.tg0 * sqrt(complex(p_.ng0_2 - ni2sin2theta)) - 618 | p_.ti0 * sqrt(complex(p_.ni0_2 - ni2sin2theta)))); 619 | dW = i * ((1.0 - p_.ni / p_.ns) * p_.ni * costheta + nsroot); 620 | 621 | tmp = expW * bessel_0 * t0; 622 | sum_I0 += tmp; 623 | sum_dzI0 += tmp * dW; 624 | tmp = expW * bessel_1 * t1; 625 | sum_I1 += tmp; 626 | sum_dzI1 += tmp * dW; 627 | tmp = expW * bessel_2 * t2; 628 | sum_I2 += tmp; 629 | sum_dzI2 += tmp * dW; 630 | 631 | sum_dxI0 += expW * bessel_1 * t0 * sintheta; 632 | sum_dxI1 += expW * (bessel_0 - bessel_2) * t1 * sintheta; 633 | sum_dxI2 += expW * (bessel_1 - bessel_3) * t2 * sintheta; 634 | } 635 | // theta = alpha; 636 | sintheta = sin(p_.alpha); 637 | costheta = cos(p_.alpha); 638 | sqrtcostheta = sqrt(costheta); 639 | nsroot = sqrt(complex(p_.ns_2 - p_.NA_2)); 640 | ngroot = sqrt(complex(p_.ng_2 - p_.NA_2)); 641 | 642 | ts1ts2 = 4.0 * p_.ni * costheta * ngroot; 643 | tp1tp2 = ts1ts2; 644 | tp1tp2 /= (p_.ng * costheta + p_.ni / p_.ng * ngroot) * 645 | (p_.ns / p_.ng * ngroot + p_.ng / p_.ns * nsroot); 646 | ts1ts2 /= (p_.ni * costheta + ngroot) * (ngroot + nsroot); 647 | 648 | bessel_0 = J0(constJ * sintheta) * sintheta * sqrtcostheta; 649 | bessel_1 = J1(constJ * sintheta) * sintheta * sqrtcostheta; 650 | if (constJ != 0.0) 651 | { 652 | bessel_2 = 2.0 * bessel_1 / (constJ * sintheta) - bessel_0; 653 | bessel_3 = 4.0 * bessel_2 / (constJ * sintheta) - bessel_1; 654 | } 655 | else 656 | { 657 | bessel_2 = 0.0; 658 | bessel_3 = 0.0; 659 | } 660 | t0 = ts1ts2 + tp1tp2 / p_.ns * nsroot; 661 | t1 = tp1tp2 * p_.ni / p_.ns * sintheta; 662 | t2 = ts1ts2 - tp1tp2 / p_.ns * nsroot; 663 | 664 | expW = exp(i * p_.k0 * 665 | ((ci - z_[k]) * sqrt(complex(p_.ni_2 - p_.NA_2)) + 666 | zp_ * nsroot + p_.tg * ngroot - 667 | p_.tg0 * sqrt(complex(p_.ng0_2 - p_.NA_2)) - 668 | p_.ti0 * sqrt(complex(p_.ni0_2 - p_.NA_2)))); 669 | dW = i * ((1.0 - p_.ni / p_.ns) * p_.ni * costheta + nsroot); 670 | 671 | tmp = expW * bessel_0 * t0; 672 | sum_I0 += tmp; 673 | sum_dzI0 += tmp * dW; 674 | tmp = expW * bessel_1 * t1; 675 | sum_I1 += tmp; 676 | sum_dzI1 += tmp * dW; 677 | tmp = expW * bessel_2 * t2; 678 | sum_I2 += tmp; 679 | sum_dzI2 += tmp * dW; 680 | 681 | sum_dxI0 += expW * bessel_1 * t0 * sintheta; 682 | sum_dxI1 += expW * (bessel_0 - bessel_2) * t1 * sintheta; 683 | sum_dxI2 += expW * (bessel_1 - bessel_3) * t2 * sintheta; 684 | 685 | if (ri > 0) 686 | { 687 | integral_[k][ri] = 688 | 8.0 * PI / 3.0 * 689 | (abs(sum_I0) * abs(sum_I0) + 2.0 * abs(sum_I1) * abs(sum_I1) + 690 | abs(sum_I2) * abs(sum_I2)) * 691 | iconst * iconst; 692 | integralDx[k][ri] = 693 | 16.0 * PI / 3.0 * p_.k0 * p_.ni * 694 | real(-sum_dxI0 * conj(sum_I0) + sum_dxI1 * conj(sum_I1) + 695 | sum_dxI2 * conj(sum_I2) / 2.0) / 696 | r * iconst * iconst; 697 | integralDz[k][ri] = 698 | 16.0 * PI / 3.0 * p_.k0 * 699 | real(conj(sum_dzI0) * sum_I0 + 2.0 * conj(sum_dzI1) * sum_I1 + 700 | conj(sum_dzI2) * sum_I2) * 701 | iconst * iconst; 702 | } 703 | else 704 | { 705 | integral_[k][0] = 706 | 8.0 * PI / 3.0 * (abs(sum_I0) * abs(sum_I0)) * iconst * iconst; 707 | integralDx[k][0] = 0.0; 708 | integralDz[k][0] = 16.0 * PI / 3.0 * p_.k0 * 709 | real(sum_I0 * conj(sum_dzI0)) * iconst * iconst; 710 | } 711 | } 712 | } // z loop 713 | 714 | // Interpolate (linear) 715 | int r0; 716 | double dr, rx, xi, yi, xd, yd; 717 | index = 0; 718 | if (p_.mode == 1) 719 | { 720 | for (int k = 0; k < nz_; ++k) 721 | { 722 | for (y = -xymax_; y <= xymax_; y++) 723 | { 724 | for (x = -xymax_; x <= xymax_; x++) 725 | { 726 | xi = (double)x - xp_; 727 | yi = (double)y - yp_; 728 | xd = xp_ - x * xystep / p_.sf; 729 | yd = yp_ - y * xystep / p_.sf; 730 | rx = sqrt(xi * xi + yi * yi); 731 | r0 = (int)rx; 732 | if (r0 + 1 < rmax_) 733 | { 734 | dr = rx - r0; 735 | index = (x + xymax_) / p_.sf + ((y + xymax_) / p_.sf) * nx_ + 736 | k * nx_ * nx_; 737 | pixels_[index] += 738 | dr * integral_[k][r0 + 1] + (1.0 - dr) * integral_[k][r0]; 739 | pixelsDxp_[index] += xd * (dr * integralDx[k][r0 + 1] + 740 | (1.0 - dr) * integralDx[k][r0]); 741 | pixelsDyp_[index] += yd * (dr * integralDx[k][r0 + 1] + 742 | (1.0 - dr) * integralDx[k][r0]); 743 | pixelsDzp_[index] += 744 | dr * integralDz[k][r0 + 1] + (1.0 - dr) * integralDz[k][r0]; 745 | } // else '0' 746 | } 747 | } 748 | } 749 | } 750 | else 751 | { 752 | for (int k = 0; k < nz_; ++k) 753 | { 754 | for (y = -xymax_; y <= xymax_; y++) 755 | { 756 | for (x = -xymax_; x <= xymax_; x++) 757 | { 758 | xi = (double)x - xp_; 759 | yi = (double)y - yp_; 760 | xd = xp_ - x * xystep_ / p_.sf; 761 | yd = yp_ - y * xystep_ / p_.sf; 762 | rx = sqrt(xi * xi + yi * yi); 763 | r0 = (int)rx; 764 | if (r0 + 1 < rmax_) 765 | { 766 | dr = rx - r0; 767 | pixels_[index] += 768 | dr * integral_[k][r0 + 1] + (1.0 - dr) * integral_[k][r0]; 769 | pixelsDxp_[index] += xd * (dr * integralDx[k][r0 + 1] + 770 | (1.0 - dr) * integralDx[k][r0]); 771 | pixelsDyp_[index] += yd * (dr * integralDx[k][r0 + 1] + 772 | (1.0 - dr) * integralDx[k][r0]); 773 | pixelsDzp_[index] += 774 | dr * integralDz[k][r0 + 1] + (1.0 - dr) * integralDz[k][r0]; 775 | } // else '0' 776 | index++; 777 | } 778 | } 779 | } 780 | } 781 | // free dynamic structures 782 | for (int k = 0; k < nz_; ++k) 783 | { 784 | delete[] integralDx[k]; 785 | delete[] integralDz[k]; 786 | } 787 | delete[] integralDx; 788 | delete[] integralDz; 789 | } // psf_dx 790 | 791 | // [h, dxp, dyp, dzp] = vectorialPSF(xp, z, nx, p) computes a vectorial 792 | // microscope point spread function model. 793 | // The partial derivatives of the model relative to the source position xp are 794 | // also calculated. The model is described in [1]. For more information and 795 | // implementation details, see [2]. 796 | // 797 | // INPUTS: 798 | // xp : Source position, 3-element vector [xp yp zp] 799 | // z : Vector of z-plane positions 800 | // nx : Window size for the psf calculation, in pixels (must be odd). 801 | // The origin is located at ((nx+1)/2, (nx+1)/2). 802 | // p : Parameter structure of system properties, with fields (case 803 | // sensitive) 804 | // ti0 : working distance of the objective 805 | // ni0 : immersion medium refractive index, design value 806 | // ni : immersion medium refractive index, experimental value 807 | // tg0 : coverslip thickness, design value 808 | // tg : coverslip thickness, experimental value 809 | // ng0 : coverslip refractive index, design value 810 | // ng : coverslip refractive index, experimental value 811 | // ns : sample refractive index 812 | // lambda : emission wavelength 813 | // M : magnification 814 | // NA : numerical aperture 815 | // pixelSize : physical size (width) of the camera pixels 816 | // f : (optional, default: 3) oversampling factor to 817 | // approximate pixel integration mode : (optional, default: 1) 818 | // if 0, returns oversampled PSF 819 | // 820 | // All spatial units are in object space, in [m]. 821 | 822 | // void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[]) 823 | // { 824 | 825 | // // Input checks 826 | // // if (nrhs!=NARGIN) mexErrMsgTxt("There must be 4 input arguments: xp, 827 | // z, w, p"); 828 | // // if ( !mxIsDouble(prhs[0]) || !mxIsDouble(prhs[1]) || 829 | // !mxIsDouble(prhs[2]) ) mexErrMsgTxt("'xp' and 'z' must be double arrays. 830 | // Window size 'w' must be an integer."); 831 | // // if ( !mxIsDouble(prhs[2]) ) mexErrMsgTxt("Input 'z' must be a double 832 | // array."); 833 | // // if ( !mxIsStruct(prhs[3]) ) mexErrMsgTxt("Input 'p' must be a 834 | // parameter structure."); 835 | 836 | // if (mxGetNumberOfElements(prhs[0])!=3) mexErrMsgTxt("Input 'xp' must be a 837 | // 3-element vector."); double* xp = mxGetPr(prhs[0]); 838 | 839 | // int nz = (int)mxGetNumberOfElements(prhs[1]); 840 | // double* z = mxGetPr(prhs[1]); 841 | 842 | // int nx = (int)mxGetScalar(prhs[2]); 843 | // if (nx%2!=1) { 844 | // mexErrMsgTxt("Windows size must be an odd integer"); 845 | // } 846 | 847 | // int np = mxGetNumberOfFields(prhs[3]); 848 | // if (np < 12) mexErrMsgTxt("Incorrect parameter vector"); 849 | // parameters p; 850 | // parseParameterStruct(p, prhs[3]); 851 | 852 | // VectorialPSF psf = VectorialPSF(xp, z, nz, nx, p); 853 | // if (nlhs==1) { 854 | // psf.calculatePSF(); 855 | // } else if (nlhs>1) { 856 | // psf.calculatePSFdxp(); 857 | // } 858 | 859 | // int ndim = 3; 860 | // if (p.mode==0) { 861 | // nx *= p.sf; 862 | // } 863 | // const mwSize dims[3] = {nx, nx, nz}; 864 | 865 | // int N = nx*nx*nz; 866 | // // copy PSF data 867 | // plhs[0] = mxCreateNumericArray(ndim, dims, mxDOUBLE_CLASS, mxREAL); 868 | // memcpy(mxGetPr(plhs[0]), psf.pixels_, N*sizeof(double)); 869 | 870 | // // copy derivatives 871 | // if (nlhs>1) { 872 | // plhs[1] = mxCreateNumericArray(ndim, dims, mxDOUBLE_CLASS, mxREAL); 873 | // memcpy(mxGetPr(plhs[1]), psf.pixelsDxp_, N*sizeof(double)); 874 | // } 875 | // if (nlhs>2) { 876 | // plhs[2] = mxCreateNumericArray(ndim, dims, mxDOUBLE_CLASS, mxREAL); 877 | // memcpy(mxGetPr(plhs[2]), psf.pixelsDyp_, N*sizeof(double)); 878 | // } 879 | // if (nlhs>3) { 880 | // plhs[3] = mxCreateNumericArray(ndim, dims, mxDOUBLE_CLASS, mxREAL); 881 | // memcpy(mxGetPr(plhs[3]), psf.pixelsDzp_, N*sizeof(double)); 882 | // } 883 | // } 884 | 885 | // compiled with: 886 | // export DYLD_LIBRARY_PATH=/Applications/MATLAB_R2013a.app/bin/maci64 && g++ 887 | // -Wall -g -DARRAY_ACCESS_INLINING -I. 888 | // -L/Applications/MATLAB_R2013a.app/bin/maci64 -I../../mex/include/ 889 | // -I/Applications/MATLAB_R2013a.app/extern/include vectorialPSF.cpp -lmx -lmex 890 | // tested with: 891 | // valgrind --tool=memcheck --leak-check=full --show-reachable=yes ./a.out 2>&1 892 | // | grep vectorialPSF 893 | -------------------------------------------------------------------------------- /src/psfmodels/__init__.py: -------------------------------------------------------------------------------- 1 | """Scalar and vectorial models of the microscope point spread function (PSF).""" 2 | 3 | try: 4 | from ._version import version as __version__ 5 | except ImportError: 6 | __version__ = "unknown" 7 | __author__ = "Talley Lambert" 8 | __email__ = "talley.lambert@gmail.com" 9 | 10 | from ._core import ( 11 | confocal_psf, 12 | make_psf, 13 | scalar_psf, 14 | scalar_psf_centered, 15 | scalarXYZFocalScan, 16 | tot_psf, 17 | vectorial_psf, 18 | vectorial_psf_centered, 19 | vectorial_psf_deriv, 20 | vectorialXYZFocalScan, 21 | ) 22 | 23 | __all__ = [ 24 | "confocal_psf", 25 | "make_psf", 26 | "scalar_psf", 27 | "scalar_psf_centered", 28 | "scalarXYZFocalScan", 29 | "tot_psf", 30 | "vectorial_psf", 31 | "vectorial_psf_centered", 32 | "vectorial_psf_deriv", 33 | "vectorialXYZFocalScan", 34 | ] 35 | -------------------------------------------------------------------------------- /src/psfmodels/_core.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from typing import Sequence, Union, cast 3 | 4 | import _psfmodels 5 | import numpy as np 6 | from scipy.signal import convolve2d 7 | from typing_extensions import Literal 8 | 9 | 10 | def make_psf( 11 | z: Union[int, Sequence[float]] = 51, 12 | nx: int = 51, 13 | *, 14 | dxy: float = 0.05, 15 | dz: float = 0.05, 16 | pz: float = 0.0, 17 | NA: float = 1.4, 18 | wvl: float = 0.6, 19 | ns: float = 1.47, 20 | ni: float = 1.515, 21 | ni0: float = 1.515, 22 | tg: float = 170, 23 | tg0: float = 170, 24 | ng: float = 1.515, 25 | ng0: float = 1.515, 26 | ti0: float = 150.0, 27 | oversample_factor: int = 3, 28 | normalize: bool = True, 29 | model: Literal["vectorial", "scalar", "gaussian"] = "vectorial", 30 | ): 31 | """Compute microscope PSF. 32 | 33 | Select the PSF model using the `model` keyword argument. Can be one of: 34 | vectorial: Vectorial PSF described by Aguet et al (2009). 35 | scalar: Scalar PSF model described by Gibson and Lanni. 36 | gaussian: Simple gaussian approximation. 37 | 38 | Parameters 39 | ---------- 40 | z : Union[int, Sequence[float]] 41 | If an integer, z is interepreted as the number of z planes to calculate, and 42 | the point source always resides in the center of the z volume (at plane ~z//2). 43 | If a sequence (list, tuple, np.array), z is interpreted as a vector of Z 44 | positions at which the PSF is calculated (in microns, relative to 45 | coverslip). 46 | When an integer is provided, `dz` may be used to change the step size. 47 | If a sequence is provided, `dz` is ignored, since the sequence already implies 48 | absolute positions relative to the coverslip. 49 | nx : int 50 | XY size of output PSF in pixels, prefer odd numbers. 51 | dxy : float 52 | pixel size in sample space (microns) 53 | dz : float 54 | axial size in sample space (microns). Only used when `z` is an integer. 55 | pz : float 56 | point source z position above the coverslip, in microns. 57 | NA : float 58 | numerical aperture of the objective lens 59 | wvl : float 60 | emission wavelength (microns) 61 | ns : float 62 | sample refractive index 63 | ni : float 64 | immersion medium refractive index, experimental value 65 | ni0 : float 66 | immersion medium refractive index, design value 67 | tg : float 68 | coverslip thickness, experimental value (microns) 69 | tg0 : float 70 | coverslip thickness, design value (microns) 71 | ng : float 72 | coverslip refractive index, experimental value 73 | ng0 : float 74 | coverslip refractive index, design value 75 | ti0 : float 76 | working distance of the objective (microns) 77 | oversample_factor : int, optional 78 | oversampling factor to approximate pixel integration, by default 3 79 | normalize : bool 80 | Whether to normalize the max value to 1. By default, True. 81 | model : str 82 | PSF model to use. Must be one of 'vectorial', 'scalar', 'gaussian'. 83 | By default 'vectorial'. 84 | 85 | Returns 86 | ------- 87 | psf : np.ndarray 88 | The PSF array with dtype np.float64 and shape (len(zv), nx, nx) 89 | """ 90 | if isinstance(z, (int, float)): 91 | zv: np.ndarray = _centered_zv(z, dz, pz) 92 | else: 93 | if dz != 0.05: 94 | warnings.warn( 95 | "dz is ignored when providing a sequence for `z`.", stacklevel=2 96 | ) 97 | zv = np.asarray(z) 98 | 99 | _args = set(_VALID_ARGS).difference({"zv", "nx", "dxy", "pz", "wvl"}) 100 | kwargs = {k: v for k, v in locals().items() if k in _args} 101 | kwargs["sf"] = oversample_factor 102 | kwargs["mode"] = 1 if oversample_factor else 0 103 | 104 | if model == "vectorial": 105 | f = vectorial_psf 106 | elif model == "scalar": 107 | f = scalar_psf 108 | elif model == "gaussian": 109 | f = gaussian_psf 110 | else: 111 | raise ValueError(f"Unrecognized psf model: {model!r}") 112 | 113 | return f(zv=zv, nx=nx, dxy=dxy, pz=pz, wvl=wvl, params=kwargs, normalize=normalize) 114 | 115 | 116 | # -------------------------------------------------------------------------------- 117 | 118 | _DEFAULT_PARAMS = { 119 | "NA": 1.4, # numerical aperture 120 | "ng0": 1.515, # coverslip RI design value 121 | "ng": 1.515, # coverslip RI experimental value 122 | "ni0": 1.515, # immersion medium RI design value 123 | "ni": 1.515, # immersion medium RI experimental value 124 | "ns": 1.47, # specimen refractive index (RI) 125 | "ti0": 150.0, # microns, working distance (immersion medium thickness) design value 126 | "tg": 170.0, # microns, coverslip thickness experimental value 127 | "tg0": 170.0, # microns, coverslip thickness design value 128 | } 129 | 130 | _VALID_ARGS = [ 131 | "zv", 132 | "nx", 133 | "pz", 134 | "ti0", 135 | "ni0", 136 | "ni", 137 | "tg0", 138 | "tg", 139 | "ng0", 140 | "ng", 141 | "ns", 142 | "wvl", 143 | "NA", 144 | "dxy", 145 | "sf", 146 | "mode", 147 | ] 148 | 149 | 150 | def _normalize_params(mp: dict): 151 | """Check and return valid microscope parameters dict, stripped of excess keys. 152 | 153 | Parameters 154 | ---------- 155 | mp : dict 156 | Dictionary of microscope parameters 157 | 158 | Raises 159 | ------ 160 | ValueError 161 | If one of the parameters is invalid. 162 | 163 | Returns 164 | ------- 165 | dict 166 | Dictionary of valid microscope parameters. 167 | """ 168 | _mp = _DEFAULT_PARAMS.copy() 169 | if mp is not None: 170 | if isinstance(mp, dict): 171 | _mp.update(mp) 172 | else: 173 | raise ValueError("mp argument must be dict of microscope params") 174 | out = {} 175 | for key, val in _mp.items(): 176 | if key in _VALID_ARGS: 177 | out[key] = val 178 | else: 179 | warnings.warn( 180 | f"parameter {key} is not one of the recognized keywords " 181 | "and is being ignored", 182 | stacklevel=2, 183 | ) 184 | 185 | if _mp["NA"] >= _mp["ni"]: 186 | raise ValueError("NA must not be greater than immersion medium RI (ni).") 187 | return {key: val for key, val in _mp.items() if key in _VALID_ARGS} 188 | 189 | 190 | def _validate_args(zv, dxy, pz): 191 | """Sanity checks for various arguments.""" 192 | if isinstance(zv, (float, int)): 193 | zv = np.array([zv]) 194 | elif isinstance(zv, (list, tuple)): 195 | zv = np.array(zv) 196 | elif not isinstance(zv, np.ndarray): 197 | raise ValueError("zd must be a scalar, iterable, or numpy array") 198 | if dxy <= 0: 199 | raise ValueError("dxy must be greater than 0") 200 | if pz < 0: 201 | raise ValueError("pz should be >= 0") 202 | return zv 203 | 204 | 205 | _infostring = """ 206 | 207 | For more information and implementation details, see: 208 | F. Aguet et al., Opt. Express 17(8), pp. 6829-6848, 2009 209 | F. Aguet, Ph.D Thesis, Swiss Federal Institute of Technology, EPFL (2009)""" 210 | 211 | _docstring = ( 212 | _infostring 213 | + """ 214 | 215 | Args: 216 | zv (float, list, np.ndarray): Float or vector of Z positions 217 | (relative to coverslip) at which PSF is calculated. Defaults to [0]. 218 | len(zv) should be odd""" 219 | ) 220 | 221 | _centerdocstring = ( 222 | _infostring 223 | + """ 224 | 225 | Args: 226 | nz (int): Z size of output PSF in pixels, must be odd. 227 | dz (float, optional): Z step size of PSF in sample space. Defaults to 0.05 228 | Kwargs:""" 229 | ) 230 | 231 | _paramdocs = """ 232 | nx (int, optional): XY size of output PSF in pixels, must be odd. Defaults to 233 | 31. dxy (float, optional): XY Pixel size in sample space (microns). Defaults to 234 | 0.05. pz (float, optional): Depth of point source relative to coverslip in 235 | microns. 236 | Defaults to 0. 237 | wvl (float, optional): Emission wavelength in microns. Defaults to 0.6. params 238 | (dict, optional): Microscope parameters dict. See optional keys below. normalize 239 | (bool, optional): Normalize PSF peak to 1. Defaults to True. 240 | 241 | valid params (all floats unless stated, all distances in microns): 242 | NA: Numerical Aperture. Defaults to 1.4 ng0: Coverslip RI design value. 243 | Defaults to 1.515 ng: Coverslip RI experimental value. Defaults to 1.515 244 | ni0: Immersion medium RI design value. Defaults to 1.515 ni: Immersion 245 | medium RI experimental value. Defaults to 1.515 ns: Specimen refractive RI. 246 | Defaults to 1.47 ti0: Working distance (immersion medium thickness) design 247 | value. 248 | Defaults to 150 249 | tg: Coverslip thickness experimental value. Defaults to 170. tg0: Coverslip 250 | thickness design value. Defaults to 170. sf (int): oversampling factor to 251 | approximate pixel integration. Defaults to 3 mode (int): if 0, returns 252 | oversampled PSF. Defaults to 1 253 | 254 | Returns: 255 | np.ndarray: The PSF volume. 256 | """ 257 | 258 | 259 | _DEFAULT_PARAMS = { 260 | "NA": 1.4, # numerical aperture 261 | "ng0": 1.515, # coverslip RI design value 262 | "ng": 1.515, # coverslip RI experimental value 263 | "ni0": 1.515, # immersion medium RI design value 264 | "ni": 1.515, # immersion medium RI experimental value 265 | "ns": 1.47, # specimen refractive index (RI) 266 | "ti0": 150.0, # microns, working distance (immersion medium thickness) design value 267 | "tg": 170.0, # microns, coverslip thickness experimental value 268 | "tg0": 170.0, # microns, coverslip thickness design value 269 | } 270 | 271 | 272 | def vectorial_psf(zv=0, nx=31, dxy=0.05, pz=0.0, wvl=0.6, params=None, normalize=True): 273 | """Compute a vectorial model of the microscope point spread function.""" 274 | zv = _validate_args(zv, dxy, pz) 275 | params = _normalize_params(params) 276 | _psf = _psfmodels.vectorial_psf(zv.copy(), int(nx), pz, wvl=wvl, dxy=dxy, **params) 277 | if normalize: 278 | _psf /= np.max(_psf) 279 | return _psf 280 | 281 | 282 | def scalar_psf(zv=0, nx=31, dxy=0.05, pz=0, wvl=0.6, params=None, normalize=True): 283 | """Compute the scalar PSF model described by Gibson and Lanni.""" 284 | zv = _validate_args(zv, dxy, pz) 285 | params = _normalize_params(params) 286 | _psf = _psfmodels.scalar_psf(zv.copy(), int(nx), pz, wvl=wvl, dxy=dxy, **params) 287 | if normalize: 288 | _psf /= np.max(_psf) 289 | return _psf 290 | 291 | 292 | def gaussian_psf(zv=0, nx=31, dxy=0.05, pz=0, wvl=0.6, params=None, normalize=True): 293 | """Approximate 3D PSF as a gaussian. 294 | 295 | Parameters derived from Zhang et al (2007). https://doi.org/10.1364/AO.46.001819 296 | Using the paraxial approximation for NA < 0.7 and the Nonparaxial approximation 297 | for NA >= 0.7. 298 | """ 299 | from scipy.stats import multivariate_normal 300 | 301 | if pz != 0: 302 | warnings.warn( 303 | "pz != 0 currently does nothing for the gaussian approximation.", 304 | stacklevel=2, 305 | ) 306 | 307 | zv = _validate_args(zv, dxy, pz) 308 | params = _normalize_params(params) 309 | 310 | na = params["NA"] 311 | nimm = params["ni"] 312 | alpha = np.arcsin(na / nimm) 313 | cosa = np.cos(alpha) 314 | Kem = 2 * np.pi / wvl 315 | 316 | if na < 0.7: 317 | # paraxial 318 | sigma_xy = np.sqrt(2) / (Kem * na) 319 | sigma_z = (2 * np.sqrt(6) * nimm) / (Kem * na**2) 320 | 321 | else: 322 | # non-parax 323 | sigma_xy = (4 - 7 * cosa**1.5 + 3 * cosa**3.5) / (7 * (1 - cosa**1.5)) 324 | sigma_xy = (1 / (nimm * Kem)) * sigma_xy**-0.5 325 | 326 | d = 4 * cosa**5 - 25 * cosa**3.5 + 42 * cosa**2.5 - 25 * cosa**1.5 + 4 327 | d = np.sqrt(6) * nimm * Kem * np.sqrt(d) 328 | sigma_z = 5 * np.sqrt(7) * (1 - cosa**1.5) / d 329 | 330 | _xycoords = _centered_zv(nx, dxy) 331 | y, z, x = np.meshgrid(_xycoords, zv, _xycoords) 332 | coords = np.column_stack([z.flat, y.flat, x.flat]) 333 | 334 | sigma = np.array([sigma_z, sigma_xy, sigma_xy]) 335 | _psf = multivariate_normal.pdf(coords, mean=[0, 0, 0], cov=np.diag(sigma**2)) 336 | _psf = _psf.reshape((len(zv), nx, nx)) 337 | 338 | if normalize: 339 | _psf /= np.max(_psf) 340 | return _psf 341 | 342 | 343 | def vectorial_psf_deriv( 344 | zv=0, nx=31, dxy=0.05, pz=0.0, wvl=0.6, params=None, normalize=True 345 | ): 346 | """Compute a vectorial model of the microscope point spread function. 347 | 348 | also returns derivatives in dx, dy, dz. 349 | 350 | Returns 351 | ------- 352 | 4-tuple of np.ndarrays: (_psf, dxp, dyp, dzp) 353 | 354 | """ 355 | zv = _validate_args(zv, dxy, pz) 356 | params = _normalize_params(params) 357 | pixdxp = np.zeros((len(zv), nx, nx)) 358 | pixdyp = np.zeros((len(zv), nx, nx)) 359 | pixdzp = np.zeros((len(zv), nx, nx)) 360 | _psf = _psfmodels.vectorial_psf_deriv( 361 | pixdxp, pixdyp, pixdzp, zv.copy(), int(nx), pz, wvl=wvl, dxy=dxy, **params 362 | ) 363 | if normalize: 364 | _psf /= np.max(_psf) 365 | return _psf, pixdxp, pixdyp, pixdzp 366 | 367 | 368 | def vectorial_psf_centered(nz, dz=0.05, **kwargs): 369 | """Compute a vectorial model of the microscope point spread function. 370 | 371 | The point source is always in the center of the output volume. 372 | """ 373 | zv = _centered_zv(nz, dz, kwargs.get("pz", 0)) 374 | return vectorial_psf(zv, **kwargs) 375 | 376 | 377 | def scalar_psf_centered(nz, dz=0.05, **kwargs): 378 | """Compute the scalar PSF model described by Gibson and Lanni. 379 | 380 | The point source is always in the center of the output volume. 381 | """ 382 | zv = _centered_zv(nz, dz, kwargs.get("pz", 0)) 383 | return scalar_psf(zv, **kwargs) 384 | 385 | 386 | def _centered_zv(nz, dz, pz=0) -> np.ndarray: 387 | lim = (nz - 1) * dz / 2 388 | return np.linspace(-lim + pz, lim + pz, nz) 389 | 390 | 391 | vectorial_psf.__doc__ += _docstring + _paramdocs # type: ignore 392 | scalar_psf.__doc__ += _docstring + _paramdocs # type: ignore 393 | vectorial_psf_centered.__doc__ += _centerdocstring + _paramdocs # type: ignore 394 | scalar_psf_centered.__doc__ += _centerdocstring + _paramdocs # type: ignore 395 | 396 | 397 | def vectorialXYZFocalScan(mp, dxy, xy_size, zv, normalize=True, pz=0.0, wvl=0.6, zd=0): 398 | """Compute a vectorial model of the microscope point spread function. 399 | 400 | This function is merely here as a convenience to mimic the MicroscPSF-Py API. 401 | """ 402 | return vectorial_psf(zv, xy_size, dxy, pz, wvl, mp, normalize) 403 | 404 | 405 | def scalarXYZFocalScan(mp, dxy, xy_size, zv, normalize=True, pz=0.0, wvl=0.6, zd=0): 406 | """Compute the scalar PSF model described by Gibson and Lanni. 407 | 408 | This function is merely here as a convenience to mimic the MicroscPSF-Py API. 409 | """ 410 | return scalar_psf(zv, xy_size, dxy, pz, wvl, mp, normalize) 411 | 412 | 413 | _otherapidoc = """ 414 | Args: 415 | mp (dict): Microscope parameters dict. See optional keys below. 416 | dxy (float): XY Pixel size in sample space (microns). 417 | xy_size (int): XY size of output PSF in pixels, must be odd. 418 | zv (float, list, np.ndarray): Float or vector of Z positions 419 | (relative to coverslip) at which PSF is calculated. Defaults to [0]. 420 | len(zv) should be odd. 421 | normalize (bool, optional): Normalize PSF peak to 1. Defaults to True. 422 | pz (float, optional): Depth of point source relative to coverslip in microns. 423 | Defaults to 0. 424 | wvl (float, optional): Emission wavelength in microns. Defaults to 0.6. 425 | zd (int, optional): Unused in this module. Just here for MicroscPSF-Py API. 426 | 427 | valid params (all floats unless stated, all distances in microns): 428 | NA: Numerical Aperture. Defaults to 1.4 429 | ng0: Coverslip RI design value. Defaults to 1.515 430 | ng: Coverslip RI experimental value. Defaults to 1.515 431 | ni0: Immersion medium RI design value. Defaults to 1.515 432 | ni: Immersion medium RI experimental value. Defaults to 1.515 433 | ns: Specimen refractive RI. Defaults to 1.47 434 | ti0: Working distance (immersion medium thickness) design value. 435 | Defaults to 150 436 | tg: Coverslip thickness experimental value. Defaults to 170. 437 | tg0: Coverslip thickness design value. Defaults to 170. 438 | sf (int): oversampling factor to approximate pixel integration. 439 | Defaults to 3 440 | mode (int): if 0, returns oversampled PSF. Defaults to 1 441 | 442 | Returns: 443 | np.ndarray: The PSF volume. 444 | """ 445 | 446 | vectorialXYZFocalScan.__doc__ += _otherapidoc # type: ignore 447 | scalarXYZFocalScan.__doc__ += _otherapidoc # type: ignore 448 | 449 | 450 | def tot_psf( 451 | nx=127, 452 | nz=127, 453 | dxy=0.05, 454 | dz=0.05, 455 | pz=0, 456 | z_offset=0, 457 | x_offset=0, 458 | ex_wvl=0.488, 459 | em_wvl=0.525, 460 | ex_params=None, 461 | em_params=None, 462 | psf_func="vectorial", 463 | ): 464 | """Simlulate a total system psf with orthogonal illumination & detection. 465 | 466 | (e.g. SPIM) 467 | 468 | Parameters 469 | ---------- 470 | nx : int, optional 471 | XY size of output PSF in pixels, must be odd. Defaults to 127. 472 | nz : int, optional 473 | Z size of output PSF in pixels, must be odd. Defaults to 127. 474 | dxy : float, optional 475 | XY Pixel size in sample space (microns). Defaults to 0.05. 476 | dz : float, optional 477 | Z step size of PSF in sample space. Defaults to 0.05 478 | pz : int, optional 479 | Depth of point source relative to coverslip in microns. Defaults to 0. 480 | z_offset : int, optional 481 | Defocus between the axial position of the excitation and the detection plane, 482 | with respect to the detection lens. Defaults to 0. 483 | x_offset : int, optional 484 | Mismatch between the focal point of the excitation beam and the point source, 485 | along the propogation direction of the excitation beam. Defaults to 0. 486 | ex_wvl : float, optional 487 | Emission wavelength in microns. Defaults to 0.488. 488 | em_wvl : float, optional 489 | Emission wavelength in microns. Defaults to 0.525. 490 | ex_params : dict, optional 491 | Microscope parameters dict for excitation. See optional keys below. 492 | em_params : dict, optional 493 | Microscope parameters dict for emission. See optional keys below. 494 | psf_func : str, optional 495 | The psf model to use. Can be any of 496 | {'vectorial', 'scalar', or 'microscpsf'}. Where 'microscpsf' uses the 497 | `gLXYZFocalScan` function from MicroscPSF-Py (if installed). Defaults to 498 | "vectorial". 499 | 500 | valid params (all floats unless stated, all distances in microns): 501 | NA: Numerical Aperture. Defaults to 0.4 for excitation and 1.1 for emission 502 | ni0: Immersion medium RI design value. Defaults to 1.33 ni: Immersion 503 | medium RI experimental value. Defaults to 1.33 ns: Specimen refractive RI. 504 | Defaults to 1.33 tg: Coverslip thickness experimental value. Defaults to 0 505 | (water immersion) tg0: Coverslip thickness design value. Defaults to 0 506 | (water immersion) ti0: Working distance (immersion medium thickness) design 507 | value. 508 | Defaults to 150 509 | ng0: Coverslip RI design value. Defaults to 1.515 ng: Coverslip RI 510 | experimental value. Defaults to 1.515 511 | 512 | Raises 513 | ------ 514 | ImportError: If `psf_func` == 'microscpsf' and MicroscPSF-Py cannot be imported 515 | ValueError: If `psf_func` is not one of {'vectorial', 'scalar', or 'microscpsf'} 516 | 517 | Returns 518 | ------- 519 | 3-tuple of np.ndarrays: ex_psf, em_psf, total_system_psf 520 | """ 521 | _x_params = _DEFAULT_PARAMS.copy() 522 | _x_params.update({"ni0": 1.33, "ni": 1.33, "ns": 1.33, "tg": 0, "tg0": 0}) 523 | _x_params["NA"] = 0.4 524 | _m_params = _x_params.copy() 525 | _m_params["NA"] = 1.1 526 | if ex_params is not None: 527 | _x_params.update(ex_params) 528 | if em_params is not None: 529 | _m_params.update(em_params) 530 | 531 | if psf_func.lower().startswith("microsc"): 532 | try: 533 | import microscPSF.microscPSF as msPSF 534 | except ImportError as e: 535 | raise ImportError( 536 | "Could not import MicroscPSF-Py. " 537 | 'Install with "pip install MicroscPSF-Py"' 538 | ) from e 539 | 540 | f = msPSF.gLXYZFocalScan 541 | _x_params["zd0"] = _x_params.get("zd0", 200.0 * 1.0e3) 542 | _x_params["M"] = _x_params.get("M", 100) 543 | _m_params["zd0"] = _m_params.get("zd0", 200.0 * 1.0e3) 544 | _m_params["M"] = _m_params.get("M", 100) 545 | elif psf_func.lower().startswith("vectorial"): 546 | f = vectorialXYZFocalScan 547 | elif psf_func.lower().startswith("scalar"): 548 | f = scalarXYZFocalScan 549 | else: 550 | raise ValueError( 551 | 'psf_func must be one of {"vectorial", "scalar", or "microscpsf"}' 552 | ) 553 | 554 | lim = (nx - 1) * dxy / 2 555 | emzvec = np.linspace(-lim + x_offset, lim + x_offset, nx) 556 | if np.mod(z_offset / dz, 1) != 0: 557 | z_offset = dz * np.round(z_offset / dz) 558 | warnings.warn( 559 | "Not Implemented: z_offset must be an even multiple of dz. " 560 | "Coercing z_offset to nearest dz multiple: %s" % z_offset, 561 | stacklevel=2, 562 | ) 563 | _zoff = int(np.ceil(z_offset / dz)) 564 | ex_nx = nz + 2 * np.abs(_zoff) 565 | exzvec = _centered_zv(nz, dz, pz) 566 | 567 | ex_psf = f(_x_params, dz, ex_nx, emzvec, pz=0, wvl=ex_wvl).T.sum(0) 568 | ex_psf = ex_psf[:nz] if _zoff >= 0 else ex_psf[-nz:] 569 | em_psf = f(_m_params, dxy, nx, exzvec, pz=pz, wvl=em_wvl) 570 | 571 | combined = ex_psf[:, :, np.newaxis] * em_psf 572 | return (ex_psf, em_psf, combined) 573 | 574 | 575 | def confocal_psf( 576 | z: Union[int, Sequence[float]] = 51, 577 | nx: int = 51, 578 | *, 579 | pinhole_au: float = 1.0, 580 | dxy: float = 0.05, 581 | dz: float = 0.05, 582 | pz: float = 0.0, 583 | NA: float = 1.4, 584 | ex_wvl: float = 0.6, 585 | em_wvl: float = 0.6, 586 | ns: float = 1.47, 587 | ni: float = 1.515, 588 | ni0: float = 1.515, 589 | tg: float = 170, 590 | tg0: float = 170, 591 | ng: float = 1.515, 592 | ng0: float = 1.515, 593 | ti0: float = 150.0, 594 | oversample_factor: int = 3, 595 | normalize: bool = True, 596 | model: Literal["vectorial", "scalar", "gaussian"] = "vectorial", 597 | pinhole_irrelevance_threshold: float = 50, 598 | ): 599 | kwargs = { 600 | "z": z, 601 | "nx": nx, 602 | "dxy": dxy, 603 | "dz": dz, 604 | "pz": pz, 605 | "NA": NA, 606 | "ex_wvl": ex_wvl, 607 | "em_wvl": em_wvl, 608 | "ns": ns, 609 | "ni": ni, 610 | "ni0": ni0, 611 | "tg": tg, 612 | "tg0": tg0, 613 | "ng": ng, 614 | "ng0": ng0, 615 | "ti0": ti0, 616 | "oversample_factor": oversample_factor, 617 | "normalize": normalize, 618 | "model": model, 619 | } 620 | _ex_wvl = cast(float, kwargs.pop("ex_wvl")) 621 | _em_wvl = cast(float, kwargs.pop("em_wvl")) 622 | ex_psf = make_psf(wvl=_ex_wvl, **kwargs) # type: ignore 623 | 624 | if pinhole_au >= pinhole_irrelevance_threshold: 625 | return ex_psf 626 | 627 | em_psf = make_psf(wvl=_em_wvl, **kwargs) # type: ignore 628 | 629 | pinhole_size = pinhole_au * 0.61 * _em_wvl / NA 630 | pinhole = _top_hat(nx, pinhole_size / dxy) 631 | 632 | # convolve em_psf with pinhole, only in XY 633 | em_psf = np.array([convolve2d(p, pinhole, mode="same") for p in em_psf]) 634 | 635 | return ex_psf * em_psf 636 | 637 | 638 | def _top_hat(nx: int, radius: float): 639 | """Return a top hat function of size (nx, nx) and radius `radius`. 640 | 641 | radius is in units of pixels. Everything inside of the radius is 1, everything 642 | outside is 0. 643 | """ 644 | x = np.arange(nx) - nx // 2 645 | xx, yy = np.meshgrid(x, x) 646 | r = np.sqrt(xx**2 + yy**2) 647 | return (r <= radius).astype(int) 648 | 649 | 650 | __all__ = [ 651 | "vectorial_psf", 652 | "vectorial_psf_deriv", 653 | "scalar_psf", 654 | "vectorial_psf_centered", 655 | "scalar_psf_centered", 656 | "vectorialXYZFocalScan", 657 | "scalarXYZFocalScan", 658 | "tot_psf", 659 | ] 660 | -------------------------------------------------------------------------------- /src/psfmodels/_cuvec.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | import numpy as np 4 | 5 | try: 6 | import cupy as xp 7 | from cupyx.scipy.ndimage import map_coordinates 8 | from cupyx.scipy.special import j0, j1 9 | except ImportError: 10 | try: 11 | import jax.numpy as xp 12 | from jax.scipy.ndimage import map_coordinates 13 | 14 | from ._jax_bessel import j0, j1 15 | 16 | except ImportError: 17 | import numpy as xp 18 | from scipy.ndimage import map_coordinates 19 | from scipy.special import j0, j1 20 | 21 | 22 | @dataclass 23 | class Objective: 24 | na: float = 1.4 # numerical aperture 25 | coverslip_ri: float = 1.515 # coverslip RI experimental value (ng) 26 | coverslip_ri_spec: float = 1.515 # coverslip RI design value (ng0) 27 | immersion_medium_ri: float = 1.515 # immersion medium RI experimental value (ni) 28 | immersion_medium_ri_spec: float = 1.515 # immersion medium RI design value (ni0) 29 | specimen_ri: float = 1.47 # specimen refractive index (ns) 30 | working_distance: float = 150.0 # um, working distance, design value (ti0) 31 | coverslip_thickness: float = 170.0 # um, coverslip thickness (tg) 32 | coverslip_thickness_spec: float = 170.0 # um, coverslip thickness design (tg0) 33 | 34 | @property 35 | def NA(self): 36 | return self.na 37 | 38 | @property 39 | def ng(self): 40 | return self.coverslip_ri 41 | 42 | @property 43 | def ng0(self): 44 | return self.coverslip_ri_spec 45 | 46 | @property 47 | def ni(self): 48 | return self.immersion_medium_ri 49 | 50 | @property 51 | def ni0(self): 52 | return self.immersion_medium_ri_spec 53 | 54 | @property 55 | def ns(self): 56 | return self.specimen_ri 57 | 58 | @property 59 | def ti0(self): 60 | return self.working_distance * 1e-6 61 | 62 | @property 63 | def tg(self): 64 | return self.coverslip_thickness * 1e-6 65 | 66 | @property 67 | def tg0(self): 68 | return self.coverslip_thickness_spec * 1e-6 69 | 70 | @property 71 | def half_angle(self): 72 | return np.arcsin(self.na / self.ni) 73 | 74 | 75 | if xp.__name__ == "jax.numpy": 76 | 77 | def _simp_like(arr): 78 | simp = xp.empty_like(arr) 79 | 80 | simp = simp.at[::2].set(4) 81 | simp = simp.at[1::2].set(2) 82 | simp = simp.at[-1].set(1) 83 | return simp 84 | 85 | def _array_assign(arr, mask, value): 86 | return arr.at[mask].set(value) 87 | 88 | else: 89 | 90 | def _simp_like(arr): 91 | simp = xp.empty_like(arr) 92 | simp[::2] = 4 93 | simp[1::2] = 2 94 | simp[-1] = 1 95 | return simp 96 | 97 | def _array_assign(arr, mask, value): 98 | arr[mask] = value 99 | return arr 100 | 101 | 102 | def simpson( 103 | p: Objective, 104 | theta: np.ndarray, 105 | constJ: np.ndarray, 106 | zv: np.ndarray, 107 | ci: float, 108 | zp: float, 109 | wave_num: float, 110 | ): 111 | # L_theta calculation 112 | sintheta = xp.sin(theta) 113 | costheta = xp.cos(theta) 114 | sqrtcostheta = xp.sqrt(costheta).astype("complex") 115 | ni2sin2theta = p.ni**2 * sintheta**2 116 | nsroot = xp.sqrt(p.ns**2 - ni2sin2theta) 117 | ngroot = xp.sqrt(p.ng**2 - ni2sin2theta) 118 | _z = zv[:, xp.newaxis, xp.newaxis] if zv.ndim else zv 119 | L0 = ( 120 | p.ni * (ci - _z) * costheta 121 | + zp * nsroot 122 | + p.tg * ngroot 123 | - p.tg0 * xp.sqrt(p.ng0**2 - ni2sin2theta) 124 | - p.ti0 * xp.sqrt(p.ni0**2 - ni2sin2theta) 125 | ) 126 | expW = xp.exp(1j * wave_num * L0) 127 | 128 | simp = _simp_like(theta) 129 | 130 | ts1ts2 = (4.0 * p.ni * costheta * ngroot).astype("complex") 131 | tp1tp2 = ts1ts2.copy() 132 | tp1tp2 /= (p.ng * costheta + p.ni / p.ng * ngroot) * ( 133 | p.ns / p.ng * ngroot + p.ng / p.ns * nsroot 134 | ) 135 | ts1ts2 /= (p.ni * costheta + ngroot) * (ngroot + nsroot) 136 | 137 | # 2.0 factor: Simpson's rule 138 | bessel_0 = simp * j0(constJ[:, xp.newaxis] * sintheta) * sintheta * sqrtcostheta 139 | bessel_1 = simp * j1(constJ[:, xp.newaxis] * sintheta) * sintheta * sqrtcostheta 140 | 141 | with np.errstate(invalid="ignore"): 142 | bessel_2 = 2.0 * bessel_1 / (constJ[:, xp.newaxis] * sintheta) - bessel_0 143 | 144 | bessel_2 = _array_assign(bessel_2, constJ == 0.0, 0) 145 | 146 | bessel_0 *= ts1ts2 + tp1tp2 / p.ns * nsroot 147 | bessel_1 *= tp1tp2 * p.ni / p.ns * sintheta 148 | bessel_2 *= ts1ts2 - tp1tp2 / p.ns * nsroot 149 | 150 | sum_I0 = xp.abs((expW * bessel_0).sum(-1)) 151 | sum_I1 = xp.abs((expW * bessel_1).sum(-1)) 152 | sum_I2 = xp.abs((expW * bessel_2).sum(-1)) 153 | 154 | return xp.real(sum_I0**2 + 2.0 * sum_I1**2 + sum_I2**2) 155 | 156 | 157 | def vectorial_rz(zv, nx=51, pos=(0, 0, 0), dxy=0.04, wvl=0.6, params=None, sf=3): 158 | p = Objective(**(params or {})) 159 | 160 | wave_num = 2 * np.pi / (wvl * 1e-6) 161 | 162 | xpos, ypos, zpos = pos 163 | 164 | # nz_ = len(z) 165 | xystep_ = dxy * 1e-6 166 | xymax = (nx * sf - 1) // 2 167 | 168 | # position in pixels 169 | xpos *= sf / xystep_ 170 | ypos *= sf / xystep_ 171 | rn = 1 + int(xp.sqrt(xpos * xpos + ypos * ypos)) 172 | rmax = int(xp.ceil(np.sqrt(2.0) * xymax) + rn + 1) # +1 for interpolation, dx, dy 173 | rvec = xp.arange(rmax) * xystep_ / sf 174 | constJ = wave_num * rvec * p.ni 175 | 176 | # CALCULATE 177 | # constant component of OPD 178 | ci = zpos * (1 - p.ni / p.ns) + p.ni * (p.tg0 / p.ng0 + p.ti0 / p.ni0 - p.tg / p.ng) 179 | 180 | nSamples = 4 * int(1.0 + p.half_angle * xp.max(constJ) / np.pi) 181 | nSamples = np.maximum(nSamples, 60) 182 | ud = 3.0 * sf 183 | 184 | step = p.half_angle / nSamples 185 | theta = xp.arange(1, nSamples + 1) * step 186 | simpson_integral = simpson(p, theta, constJ, zv, ci, zpos, wave_num) 187 | return 8.0 * np.pi / 3.0 * simpson_integral * (step / ud) ** 2 188 | 189 | # except xp.cuda.memory.OutOfMemoryError: 190 | # integral = xp.empty((len(z), rmax)) 191 | # for k, zpos in enumerate(z): 192 | # simp = simpson(p_, nSamples, constJ, zpos, ci, zp_) 193 | # step = p.half_angle / nSamples 194 | # integral[k] = 8.0 * np.pi / 3.0 * simp * (step / ud) ** 2 195 | # del simp 196 | 197 | 198 | def radius_map(shape, off=None): 199 | if off is not None: 200 | offy, offx = off 201 | else: 202 | off = (0, 0) 203 | ny, nx = shape 204 | yi, xi = xp.mgrid[:ny, :nx] 205 | yi = yi - (ny - 1) / 2 - offy 206 | xi = xi - (nx - 1) / 2 - offx 207 | return xp.hypot(yi, xi) 208 | 209 | 210 | def rz_to_xyz(rz, xyshape, sf=3, off=None): 211 | """Use interpolation to create a 3D XYZ PSF from a 2D ZR PSF.""" 212 | # Create XY grid of radius values. 213 | rmap = radius_map(xyshape, off) * sf 214 | nz = rz.shape[0] 215 | out = xp.zeros((nz, *xyshape)) 216 | out = [] 217 | for z in range(nz): 218 | o = map_coordinates( 219 | rz, xp.asarray([xp.ones(rmap.size) * z, rmap.ravel()]), order=1 220 | ).reshape(xyshape) 221 | out.append(o) 222 | 223 | out = xp.asarray(out) 224 | return out.get() if hasattr(out, "get") else out 225 | 226 | 227 | # def rz_to_xyz(rz, xyshape, sf=3, off=None): 228 | # """Use interpolation to create a 3D XYZ PSF from a 2D ZR PSF.""" 229 | # # Create XY grid of radius values. 230 | # rmap = radius_map(xyshape, off) * sf 231 | # ny, nx = xyshape 232 | # nz, nr = rz.shape 233 | # ZZ, RR = xp.meshgrid(xp.arange(nz, dtype="float64"), rmap.ravel()) 234 | # o = map_coordinates(rz, xp.array([ZZ.ravel(), RR.ravel()]), order=1) 235 | # return o.reshape((nx, ny, nz)).T 236 | 237 | 238 | def vectorial_psf( 239 | zv, 240 | nx=31, 241 | ny=None, 242 | pos=(0, 0, 0), 243 | dxy=0.05, 244 | wvl=0.6, 245 | params=None, 246 | sf=3, 247 | normalize=True, 248 | ): 249 | zv = xp.asarray(zv * 1e-6) # convert to meters 250 | ny = ny or nx 251 | rz = vectorial_rz(zv, np.maximum(ny, nx), pos, dxy, wvl, params, sf) 252 | _psf = rz_to_xyz(rz, (ny, nx), sf, off=np.array(pos[:2]) / (dxy * 1e-6)) 253 | if normalize: 254 | _psf /= xp.max(_psf) 255 | return _psf 256 | 257 | 258 | def _centered_zv(nz, dz, pz=0) -> np.ndarray: 259 | lim = (nz - 1) * dz / 2 260 | return np.linspace(-lim + pz, lim + pz, nz) 261 | 262 | 263 | def vectorial_psf_centered(nz, dz=0.05, **kwargs): 264 | """Compute a vectorial model of the microscope point spread function. 265 | 266 | The point source is always in the center of the output volume. 267 | """ 268 | zv = _centered_zv(nz, dz, kwargs.get("pz", 0)) 269 | return vectorial_psf(zv, **kwargs) 270 | 271 | 272 | if __name__ == "__main__": 273 | zv = np.linspace(-3, 3, 61) 274 | from time import perf_counter 275 | 276 | t0 = perf_counter() 277 | psf = vectorial_psf(zv, nx=512) 278 | t1 = perf_counter() 279 | print(psf.shape) 280 | print(t1 - t0) 281 | assert np.allclose(np.load("out.npy"), psf, atol=0.1) 282 | -------------------------------------------------------------------------------- /src/psfmodels/_jax_bessel.py: -------------------------------------------------------------------------------- 1 | """Code from Benjamin Pope. 2 | 3 | MIT License 4 | 5 | https://github.com/benjaminpope/sibylla/blob/209a1962e2cfd297c53fce7cc470dfb271bc4c6b/notebooks/bessel_test.ipynb 6 | """ 7 | import jax.numpy as jnp 8 | from jax import config, jit 9 | 10 | # this is *absolutely essential* for the jax bessel function to be numerically stable 11 | config.update("jax_enable_x64", True) 12 | 13 | __all__ = ["j0", "j1"] 14 | 15 | 16 | @jit 17 | def j1(x): 18 | """Bessel function of order one - using the implementation from CEPHES.""" 19 | return jnp.sign(x) * jnp.where( 20 | jnp.abs(x) < 5.0, _j1_small(jnp.abs(x)), _j1_large_c(jnp.abs(x)) 21 | ) 22 | 23 | 24 | @jit 25 | def j0(x): 26 | """Implementation of J0 for all x in Jax.""" 27 | return jnp.where(jnp.abs(x) < 5.0, _j0_small(jnp.abs(x)), _j0_large(jnp.abs(x))) 28 | 29 | 30 | RP1 = jnp.array( 31 | [ 32 | -8.99971225705559398224e8, 33 | 4.52228297998194034323e11, 34 | -7.27494245221818276015e13, 35 | 3.68295732863852883286e15, 36 | ] 37 | ) 38 | RQ1 = jnp.array( 39 | [ 40 | 1.0, 41 | 6.20836478118054335476e2, 42 | 2.56987256757748830383e5, 43 | 8.35146791431949253037e7, 44 | 2.21511595479792499675e10, 45 | 4.74914122079991414898e12, 46 | 7.84369607876235854894e14, 47 | 8.95222336184627338078e16, 48 | 5.32278620332680085395e18, 49 | ] 50 | ) 51 | PP1 = jnp.array( 52 | [ 53 | 7.62125616208173112003e-4, 54 | 7.31397056940917570436e-2, 55 | 1.12719608129684925192e0, 56 | 5.11207951146807644818e0, 57 | 8.42404590141772420927e0, 58 | 5.21451598682361504063e0, 59 | 1.00000000000000000254e0, 60 | ] 61 | ) 62 | PQ1 = jnp.array( 63 | [ 64 | 5.71323128072548699714e-4, 65 | 6.88455908754495404082e-2, 66 | 1.10514232634061696926e0, 67 | 5.07386386128601488557e0, 68 | 8.39985554327604159757e0, 69 | 5.20982848682361821619e0, 70 | 9.99999999999999997461e-1, 71 | ] 72 | ) 73 | 74 | QP1 = jnp.array( 75 | [ 76 | 5.10862594750176621635e-2, 77 | 4.98213872951233449420e0, 78 | 7.58238284132545283818e1, 79 | 3.66779609360150777800e2, 80 | 7.10856304998926107277e2, 81 | 5.97489612400613639965e2, 82 | 2.11688757100572135698e2, 83 | 2.52070205858023719784e1, 84 | ] 85 | ) 86 | QQ1 = jnp.array( 87 | [ 88 | 1.0, 89 | 7.42373277035675149943e1, 90 | 1.05644886038262816351e3, 91 | 4.98641058337653607651e3, 92 | 9.56231892404756170795e3, 93 | 7.99704160447350683650e3, 94 | 2.82619278517639096600e3, 95 | 3.36093607810698293419e2, 96 | ] 97 | ) 98 | PP0 = jnp.array( 99 | [ 100 | 7.96936729297347051624e-4, 101 | 8.28352392107440799803e-2, 102 | 1.23953371646414299388e0, 103 | 5.44725003058768775090e0, 104 | 8.74716500199817011941e0, 105 | 5.30324038235394892183e0, 106 | 9.99999999999999997821e-1, 107 | ] 108 | ) 109 | PQ0 = jnp.array( 110 | [ 111 | 9.24408810558863637013e-4, 112 | 8.56288474354474431428e-2, 113 | 1.25352743901058953537e0, 114 | 5.47097740330417105182e0, 115 | 8.76190883237069594232e0, 116 | 5.30605288235394617618e0, 117 | 1.00000000000000000218e0, 118 | ] 119 | ) 120 | QP0 = jnp.array( 121 | [ 122 | -1.13663838898469149931e-2, 123 | -1.28252718670509318512e0, 124 | -1.95539544257735972385e1, 125 | -9.32060152123768231369e1, 126 | -1.77681167980488050595e2, 127 | -1.47077505154951170175e2, 128 | -5.14105326766599330220e1, 129 | -6.05014350600728481186e0, 130 | ] 131 | ) 132 | QQ0 = jnp.array( 133 | [ 134 | 1.0, 135 | 6.43178256118178023184e1, 136 | 8.56430025976980587198e2, 137 | 3.88240183605401609683e3, 138 | 7.24046774195652478189e3, 139 | 5.93072701187316984827e3, 140 | 2.06209331660327847417e3, 141 | 2.42005740240291393179e2, 142 | ] 143 | ) 144 | RP0 = jnp.array( 145 | [ 146 | -4.79443220978201773821e9, 147 | 1.95617491946556577543e12, 148 | -2.49248344360967716204e14, 149 | 9.70862251047306323952e15, 150 | ] 151 | ) 152 | RQ0 = jnp.array( 153 | [ 154 | 1.0, 155 | 4.99563147152651017219e2, 156 | 1.73785401676374683123e5, 157 | 4.84409658339962045305e7, 158 | 1.11855537045356834862e10, 159 | 2.11277520115489217587e12, 160 | 3.10518229857422583814e14, 161 | 3.18121955943204943306e16, 162 | 1.71086294081043136091e18, 163 | ] 164 | ) 165 | 166 | Z1 = 1.46819706421238932572e1 167 | Z2 = 4.92184563216946036703e1 168 | PIO4 = 0.78539816339744830962 # pi/4 169 | THPIO4 = 2.35619449019234492885 # 3*pi/4 170 | SQ2OPI = 0.79788456080286535588 # sqrt(2/pi) 171 | DR10 = 5.78318596294678452118e0 172 | DR20 = 3.04712623436620863991e1 173 | 174 | 175 | def _j1_small(x): 176 | z = x * x 177 | w = jnp.polyval(RP1, z) / jnp.polyval(RQ1, z) 178 | w = w * x * (z - Z1) * (z - Z2) 179 | return w 180 | 181 | 182 | def _j1_large_c(x): 183 | w = 5.0 / x 184 | z = w * w 185 | p = jnp.polyval(PP1, z) / jnp.polyval(PQ1, z) 186 | q = jnp.polyval(QP1, z) / jnp.polyval(QQ1, z) 187 | xn = x - THPIO4 188 | p = p * jnp.cos(xn) - w * q * jnp.sin(xn) 189 | return p * SQ2OPI / jnp.sqrt(x) 190 | 191 | 192 | def _j0_small(x): 193 | """Implementation of J0 for x < 5.""" 194 | z = x * x 195 | # if x < 1.0e-5: 196 | # return 1.0 - z/4.0 197 | 198 | p = (z - DR10) * (z - DR20) 199 | p = p * jnp.polyval(RP0, z) / jnp.polyval(RQ0, z) 200 | return jnp.where(x < 1e-5, 1 - z / 4.0, p) 201 | 202 | 203 | def _j0_large(x): 204 | """Implementation of J0 for x >= 5.""" 205 | w = 5.0 / x 206 | q = 25.0 / (x * x) 207 | p = jnp.polyval(PP0, q) / jnp.polyval(PQ0, q) 208 | q = jnp.polyval(QP0, q) / jnp.polyval(QQ0, q) 209 | xn = x - PIO4 210 | p = p * jnp.cos(xn) - w * q * jnp.sin(xn) 211 | return p * SQ2OPI / jnp.sqrt(x) 212 | -------------------------------------------------------------------------------- /src/psfmodels/_napari.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import TYPE_CHECKING 3 | 4 | from typing_extensions import Annotated 5 | 6 | from . import _core 7 | 8 | if TYPE_CHECKING: 9 | import napari.types 10 | 11 | 12 | class PSFModel(Enum): 13 | vectorial = "vectorial" 14 | scalar = "scalar" 15 | gaussian = "gaussian" 16 | 17 | 18 | dRI = {"min": 1, "max": 1.7, "step": 0.001} 19 | 20 | 21 | def make_psf( 22 | model: Annotated[PSFModel, {"label": "PSF Model"}] = PSFModel.vectorial, 23 | nz: Annotated[int, {"min": 1, "max": 1025, "label": "Num. pixels (Z)"}] = 101, 24 | nx: Annotated[int, {"min": 1, "max": 1025, "label": "Num. pixels (XY)"}] = 101, 25 | dz: Annotated[ 26 | float, {"min": 0.01, "max": 0.5, "step": 0.01, "label": "Pix size Z (µm)"} 27 | ] = 0.05, 28 | dxy: Annotated[ 29 | float, {"min": 0.01, "max": 0.5, "step": 0.01, "label": "Pix size XY (µm)"} 30 | ] = 0.05, 31 | NA: Annotated[float, {"min": 0.1, "max": 1.7, "step": 0.01}] = 1.4, 32 | wvl: Annotated[ 33 | float, {"min": 0.3, "max": 0.9, "step": 0.005, "label": "Wavelength (µm)"} 34 | ] = 0.6, 35 | pz: Annotated[float, {"max": 200, "label": "Depth from CS (µm)"}] = 0.0, 36 | ns: Annotated[float, {**dRI, "label": "Sample RI"}] = 1.47, 37 | ni: Annotated[float, {**dRI, "label": "Imm. RI"}] = 1.515, 38 | ni0: Annotated[float, {**dRI, "label": "Imm. RI (spec)"}] = 1.515, 39 | tg: Annotated[int, {"min": 0, "max": 200, "label": "CS thickness (µm)"}] = 170, 40 | tg0: Annotated[int, {"min": 0, "max": 200, "label": "CS thickness (spec)"}] = 170, 41 | ng: Annotated[float, {**dRI, "label": "CS RI"}] = 1.515, 42 | ng0: Annotated[float, {**dRI, "label": "CS RI (spec)"}] = 1.515, 43 | ) -> "napari.types.ImageData": 44 | """Generate 3D microscope PSF. 45 | 46 | Select from one of the following PSF models: 47 | vectorial: Vectorial PSF (Aguet et al, 2009) 48 | scalar: Scalar PSF model (Gibson & Lanni, 1992) 49 | gaussian: Gaussian approximation (Zhang et al, 2007) 50 | 51 | Parameters 52 | ---------- 53 | model : str 54 | PSF model to use 55 | vectorial - Vectorial PSF (Aguet et al, 2009) 56 | scalar - Scalar PSF (Gibson & Lanni, 1992) 57 | gaussian - Gaussian approximation (Zhang et al, 2007) 58 | nz : Union[int, Sequence[float]] 59 | Number of z planes 60 | nx : int 61 | Number of XY pixels, (prefer odd numbers) 62 | dxy : float 63 | pixel size (µm) 64 | dz : float 65 | Z step size (µm) 66 | pz : float 67 | Point source position above the coverslip (µm) 68 | NA : float 69 | Numerical aperture of the objective lens 70 | wvl : float 71 | Emission wavelength (µm) 72 | ns : float 73 | Sample refractive index 74 | ni : float 75 | Immersion medium refractive index 76 | ni0 : float 77 | Immersion medium refractive index (design value) 78 | tg : float 79 | Coverslip thickness (µm) 80 | tg0 : float 81 | Coverslip thickness (design value, µm) 82 | ng : float 83 | Coverslip refractive index 84 | ng0 : float 85 | Coverslip refractive index (design value) 86 | """ 87 | kwargs = locals().copy() 88 | kwargs["model"] = kwargs["model"].value 89 | kwargs["z"] = kwargs.pop("nz") 90 | return _core.make_psf(**kwargs) 91 | -------------------------------------------------------------------------------- /src/psfmodels/napari.yaml: -------------------------------------------------------------------------------- 1 | name: psfmodels 2 | contributions: 3 | commands: 4 | - id: psfmodels.make_psf 5 | title: Generate 3D Point Spread Function 6 | python_name: psfmodels._napari:make_psf 7 | widgets: 8 | - command: psfmodels.make_psf 9 | display_name: PSF Generator 10 | autogenerate: true 11 | -------------------------------------------------------------------------------- /src/psfmodels/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tlambert03/PSFmodels/dfe2b6f2e829ef6351c75c5995e7fcf7b3ef2edc/src/psfmodels/py.typed -------------------------------------------------------------------------------- /tests/test_napari_plugin.py: -------------------------------------------------------------------------------- 1 | import platform 2 | 3 | import pytest 4 | 5 | try: 6 | import qtpy # noqa 7 | except ImportError: 8 | pytest.skip("qtpy not installed", allow_module_level=True) 9 | 10 | 11 | @pytest.mark.skipif(platform.system() == "Linux", reason="annoying on ubuntu CI") 12 | def test_napari_widget(monkeypatch): 13 | from magicgui import magicgui 14 | from psfmodels._napari import make_psf 15 | 16 | with monkeypatch.context() as m: 17 | # avoid no module named 'napari' error 18 | m.setitem(make_psf.__annotations__, "return", None) 19 | wdg = magicgui(make_psf) 20 | assert wdg().shape == (101, 101, 101) 21 | -------------------------------------------------------------------------------- /tests/test_psf.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import psfmodels as psfm 3 | import pytest 4 | 5 | 6 | @pytest.mark.parametrize("model", ["vectorial", "scalar", "gaussian"]) 7 | @pytest.mark.parametrize("z", [np.linspace(-2, 2, 15), 15]) 8 | def test_make_psf(model, z): 9 | p = psfm.make_psf(z, nx=31, model=model) 10 | assert p.shape == (15, 31, 31) 11 | 12 | 13 | def test_vectorial_psf(): 14 | zvec = np.linspace(-1, 1, 5) 15 | p = psfm.vectorial_psf(zvec, nx=31) 16 | assert p.shape == (5, 31, 31) 17 | 18 | 19 | def test_vectorial_psf_deriv(): 20 | zvec = np.linspace(-1, 1, 5) 21 | results = psfm.vectorial_psf_deriv(zvec, nx=31) 22 | assert len(results) == 4 23 | assert all(x.shape == (5, 31, 31) for x in results) 24 | 25 | 26 | def test_vectorial_psf_centered(): 27 | p = psfm.vectorial_psf_centered(nx=31, nz=5) 28 | assert p.shape == (5, 31, 31) 29 | 30 | 31 | def test_scalar_psf(): 32 | zvec = np.linspace(-1, 1, 5) 33 | p = psfm.scalar_psf(zvec, nx=31) 34 | assert p.shape == (5, 31, 31) 35 | 36 | 37 | def test_scalar_psf_centered(): 38 | p = psfm.scalar_psf_centered(nx=31, nz=5) 39 | assert p.shape == (5, 31, 31) 40 | 41 | 42 | def test_confocal_psf(): 43 | zvec = np.linspace(-1, 1, 5) 44 | p = psfm.confocal_psf(zvec, nx=31, pinhole_au=0.2) 45 | assert p.shape == (5, 31, 31) 46 | p = psfm.confocal_psf(zvec, nx=31, pinhole_au=3) 47 | assert p.shape == (5, 31, 31) 48 | -------------------------------------------------------------------------------- /tests/test_purepy.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import psfmodels as psfm 3 | from psfmodels import _cuvec as pure 4 | 5 | 6 | def test_equality(): 7 | N = 101 8 | dx = 0.001 9 | zv = np.linspace(-2, 2, N) 10 | a = psfm.vectorial_psf(zv, nx=N, dxy=dx) 11 | b = pure.vectorial_psf(zv, nx=N, dxy=dx) 12 | # note, similarity gets worse as dx goes up... 13 | # hints at an interpolation difference 14 | np.testing.assert_allclose(a, b, rtol=0.005) 15 | --------------------------------------------------------------------------------