├── .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 | [](https://pypi.org/project/psfmodels)
4 | [](https://python.org)
6 | [](https://github.com/tlambert03/psfmodels/actions/workflows/ci.yml)
7 | [](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 | 
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, ?it/s]\u001b[A\u001b[A\n",
71 | "\n",
72 | "\n",
73 | " 0%| | 0/3 [00:00, ?it/s]\u001b[A\u001b[A\u001b[A\n",
74 | "\n",
75 | "\n",
76 | " 33%|███▎ | 1/3 [00:01<00:02, 1.03s/it]\u001b[A\u001b[A\u001b[A\n",
77 | "\n",
78 | "\n",
79 | " 67%|██████▋ | 2/3 [00:02<00:01, 1.02s/it]\u001b[A\u001b[A\u001b[A\n",
80 | "\n",
81 | "\n",
82 | "100%|██████████| 3/3 [00:02<00:00, 1.00it/s]\u001b[A\u001b[A\u001b[A\n",
83 | "\n",
84 | "\n",
85 | " 20%|██ | 1/5 [00:03<00:12, 3.04s/it]\u001b[A\u001b[A\n",
86 | "\n",
87 | "\n",
88 | " 0%| | 0/3 [00:00, ?it/s]\u001b[A\u001b[A\u001b[A\n",
89 | "\n",
90 | "\n",
91 | " 33%|███▎ | 1/3 [00:01<00:02, 1.03s/it]\u001b[A\u001b[A\u001b[A\n",
92 | "\n",
93 | "\n",
94 | " 67%|██████▋ | 2/3 [00:02<00:01, 1.02s/it]\u001b[A\u001b[A\u001b[A\n",
95 | "\n",
96 | "\n",
97 | "100%|██████████| 3/3 [00:03<00:00, 1.01s/it]\u001b[A\u001b[A\u001b[A\n",
98 | "\n",
99 | "\n",
100 | " 40%|████ | 2/5 [00:06<00:09, 3.05s/it]\u001b[A\u001b[A\n",
101 | "\n",
102 | "\n",
103 | " 0%| | 0/3 [00:00, ?it/s]\u001b[A\u001b[A\u001b[A\n",
104 | "\n",
105 | "\n",
106 | " 33%|███▎ | 1/3 [00:01<00:02, 1.06s/it]\u001b[A\u001b[A\u001b[A\n",
107 | "\n",
108 | "\n",
109 | " 67%|██████▋ | 2/3 [00:02<00:01, 1.07s/it]\u001b[A\u001b[A\u001b[A\n",
110 | "\n",
111 | "\n",
112 | "100%|██████████| 3/3 [00:03<00:00, 1.16s/it]\u001b[A\u001b[A\u001b[A\n",
113 | "\n",
114 | "\n",
115 | " 60%|██████ | 3/5 [00:09<00:06, 3.20s/it]\u001b[A\u001b[A\n",
116 | "\n",
117 | "\n",
118 | " 0%| | 0/3 [00:00, ?it/s]\u001b[A\u001b[A\u001b[A\n",
119 | "\n",
120 | "\n",
121 | " 33%|███▎ | 1/3 [00:01<00:03, 1.71s/it]\u001b[A\u001b[A\u001b[A\n",
122 | "\n",
123 | "\n",
124 | " 67%|██████▋ | 2/3 [00:05<00:02, 2.44s/it]\u001b[A\u001b[A\u001b[A\n",
125 | "\n",
126 | "\n",
127 | "100%|██████████| 3/3 [00:49<00:00, 16.36s/it]\u001b[A\u001b[A\u001b[A\n",
128 | "\n",
129 | "\n",
130 | " 80%|████████ | 4/5 [00:59<00:17, 17.22s/it]\u001b[A\u001b[A\n",
131 | "\n",
132 | "\n",
133 | " 0%| | 0/3 [00:00, ?it/s]\u001b[A\u001b[A\u001b[A\n",
134 | "\n",
135 | "\n",
136 | " 33%|███▎ | 1/3 [00:13<00:26, 13.22s/it]\u001b[A\u001b[A\u001b[A\n",
137 | "\n",
138 | "\n",
139 | " 67%|██████▋ | 2/3 [00:28<00:13, 13.96s/it]\u001b[A\u001b[A\u001b[A\n",
140 | "\n",
141 | "\n",
142 | "100%|██████████| 3/3 [06:19<00:00, 126.46s/it]\u001b[A\u001b[A\u001b[A\n",
143 | "\n",
144 | "\n",
145 | "100%|██████████| 5/5 [07:25<00:00, 89.12s/it] \u001b[A\u001b[A\n"
146 | ]
147 | },
148 | {
149 | "data": {
150 | "image/png": "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\n",
151 | "text/plain": [
152 | ""
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 |
--------------------------------------------------------------------------------