phaserEM 0.1__tar.gz → 0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. {phaserem-0.1 → phaserem-0.2}/PKG-INFO +83 -16
  2. phaserem-0.2/README.md +130 -0
  3. {phaserem-0.1 → phaserem-0.2}/phaser/engines/common/noise_models.py +3 -4
  4. {phaserem-0.1 → phaserem-0.2}/phaser/engines/common/output.py +69 -5
  5. {phaserem-0.1 → phaserem-0.2}/phaser/engines/common/regularizers.py +106 -4
  6. {phaserem-0.1 → phaserem-0.2}/phaser/engines/common/simulation.py +41 -5
  7. phaserem-0.2/phaser/engines/conventional/run.py +123 -0
  8. {phaserem-0.1 → phaserem-0.2}/phaser/engines/conventional/solvers.py +39 -11
  9. {phaserem-0.1 → phaserem-0.2}/phaser/engines/gradient/run.py +122 -135
  10. {phaserem-0.1 → phaserem-0.2}/phaser/execute.py +190 -95
  11. {phaserem-0.1 → phaserem-0.2}/phaser/hooks/__init__.py +60 -5
  12. {phaserem-0.1 → phaserem-0.2}/phaser/hooks/io/empad.py +15 -5
  13. phaserem-0.2/phaser/hooks/io/manual.py +168 -0
  14. {phaserem-0.1 → phaserem-0.2}/phaser/hooks/preprocessing.py +18 -10
  15. {phaserem-0.1 → phaserem-0.2}/phaser/hooks/regularization.py +25 -2
  16. {phaserem-0.1 → phaserem-0.2}/phaser/hooks/scan.py +2 -1
  17. phaserem-0.2/phaser/hooks/tilt.py +55 -0
  18. {phaserem-0.1 → phaserem-0.2}/phaser/io/empad.py +29 -10
  19. {phaserem-0.1 → phaserem-0.2}/phaser/main.py +1 -1
  20. phaserem-0.2/phaser/observer.py +332 -0
  21. {phaserem-0.1 → phaserem-0.2}/phaser/plan.py +23 -4
  22. {phaserem-0.1 → phaserem-0.2}/phaser/state.py +38 -3
  23. {phaserem-0.1 → phaserem-0.2}/phaser/types.py +6 -3
  24. {phaserem-0.1 → phaserem-0.2}/phaser/utils/_jax_kernels.py +5 -1
  25. {phaserem-0.1 → phaserem-0.2}/phaser/utils/analysis.py +36 -7
  26. {phaserem-0.1 → phaserem-0.2}/phaser/utils/image.py +43 -13
  27. {phaserem-0.1 → phaserem-0.2}/phaser/utils/io.py +8 -4
  28. {phaserem-0.1 → phaserem-0.2}/phaser/utils/num.py +24 -1
  29. {phaserem-0.1 → phaserem-0.2}/phaser/utils/object.py +11 -10
  30. {phaserem-0.1 → phaserem-0.2}/phaser/utils/plotting.py +13 -6
  31. phaserem-0.2/phaser/web/dist/bundle-dashboard.js +3577 -0
  32. phaserem-0.2/phaser/web/dist/bundle-manager.js +3455 -0
  33. phaserem-0.2/phaser/web/dist/bundle-vendors-node_modules_wasm-array_wasm_array_js.js +106 -0
  34. {phaserem-0.1 → phaserem-0.2}/phaser/web/routes.py +21 -4
  35. {phaserem-0.1 → phaserem-0.2}/phaser/web/server.py +81 -16
  36. {phaserem-0.1 → phaserem-0.2}/phaser/web/slurm.py +3 -0
  37. {phaserem-0.1 → phaserem-0.2}/phaser/web/templates/base.html +1 -1
  38. {phaserem-0.1 → phaserem-0.2}/phaser/web/templates/dashboard.html +0 -4
  39. phaserem-0.2/phaser/web/templates/manager.html +10 -0
  40. {phaserem-0.1 → phaserem-0.2}/phaser/web/types.py +13 -9
  41. {phaserem-0.1 → phaserem-0.2}/phaser/web/worker.py +14 -9
  42. {phaserem-0.1 → phaserem-0.2}/phaserEM.egg-info/PKG-INFO +83 -16
  43. {phaserem-0.1 → phaserem-0.2}/phaserEM.egg-info/SOURCES.txt +4 -2
  44. {phaserem-0.1 → phaserem-0.2}/phaserEM.egg-info/requires.txt +12 -8
  45. {phaserem-0.1 → phaserem-0.2}/pyproject.toml +17 -11
  46. {phaserem-0.1 → phaserem-0.2}/tests/test_initialization.py +57 -11
  47. phaserem-0.2/tests/test_load.py +150 -0
  48. {phaserem-0.1 → phaserem-0.2}/tests/test_num.py +16 -3
  49. phaserem-0.1/README.md +0 -69
  50. phaserem-0.1/phaser/engines/conventional/run.py +0 -142
  51. phaserem-0.1/phaser/web/dist/03510a839ccb97b0da9f.module.wasm +0 -0
  52. phaserem-0.1/phaser/web/dist/bundle-dashboard.js +0 -712
  53. phaserem-0.1/phaser/web/dist/bundle-manager.js +0 -210
  54. phaserem-0.1/phaser/web/dist/bundle-vendors-node_modules_wasm-array_wasm_array_js.js +0 -106
  55. phaserem-0.1/phaser/web/dist/style.css +0 -152
  56. phaserem-0.1/phaser/web/templates/manager.html +0 -19
  57. {phaserem-0.1 → phaserem-0.2}/LICENSE.txt +0 -0
  58. {phaserem-0.1 → phaserem-0.2}/phaser/__init__.py +0 -0
  59. {phaserem-0.1 → phaserem-0.2}/phaser/__main__.py +0 -0
  60. {phaserem-0.1 → phaserem-0.2}/phaser/engines/common/__init__.py +0 -0
  61. {phaserem-0.1 → phaserem-0.2}/phaser/engines/common/position_correction.py +0 -0
  62. {phaserem-0.1 → phaserem-0.2}/phaser/engines/conventional/__init__.py +0 -0
  63. {phaserem-0.1 → phaserem-0.2}/phaser/engines/gradient/solvers.py +0 -0
  64. {phaserem-0.1 → phaserem-0.2}/phaser/hooks/hook.py +0 -0
  65. {phaserem-0.1 → phaserem-0.2}/phaser/hooks/object.py +0 -0
  66. {phaserem-0.1 → phaserem-0.2}/phaser/hooks/probe.py +0 -0
  67. {phaserem-0.1 → phaserem-0.2}/phaser/hooks/schedule.py +0 -0
  68. {phaserem-0.1 → phaserem-0.2}/phaser/hooks/solver.py +0 -0
  69. {phaserem-0.1 → phaserem-0.2}/phaser/io/__init__.py +0 -0
  70. {phaserem-0.1 → phaserem-0.2}/phaser/py.typed +0 -0
  71. {phaserem-0.1 → phaserem-0.2}/phaser/utils/__init__.py +0 -0
  72. {phaserem-0.1 → phaserem-0.2}/phaser/utils/_cuda_kernels.py +0 -0
  73. {phaserem-0.1 → phaserem-0.2}/phaser/utils/misc.py +0 -0
  74. {phaserem-0.1 → phaserem-0.2}/phaser/utils/optics.py +0 -0
  75. {phaserem-0.1 → phaserem-0.2}/phaser/utils/physics.py +0 -0
  76. {phaserem-0.1 → phaserem-0.2}/phaser/utils/scan.py +0 -0
  77. {phaserem-0.1 → phaserem-0.2}/phaser/web/__init__.py +0 -0
  78. {phaserem-0.1 → phaserem-0.2}/phaser/web/dist/9573273f862f4f5d9644.module.wasm +0 -0
  79. {phaserem-0.1 → phaserem-0.2}/phaser/web/notebook.py +0 -0
  80. {phaserem-0.1 → phaserem-0.2}/phaser/web/util.py +0 -0
  81. {phaserem-0.1 → phaserem-0.2}/phaserEM.egg-info/dependency_links.txt +0 -0
  82. {phaserem-0.1 → phaserem-0.2}/phaserEM.egg-info/entry_points.txt +0 -0
  83. {phaserem-0.1 → phaserem-0.2}/phaserEM.egg-info/top_level.txt +0 -0
  84. {phaserem-0.1 → phaserem-0.2}/setup.cfg +0 -0
  85. {phaserem-0.1 → phaserem-0.2}/setup.py +0 -0
  86. {phaserem-0.1 → phaserem-0.2}/tests/test_empad.py +0 -0
  87. {phaserem-0.1 → phaserem-0.2}/tests/test_image.py +0 -0
  88. {phaserem-0.1 → phaserem-0.2}/tests/test_misc.py +0 -0
  89. {phaserem-0.1 → phaserem-0.2}/tests/test_object.py +0 -0
  90. {phaserem-0.1 → phaserem-0.2}/tests/test_optics.py +0 -0
  91. {phaserem-0.1 → phaserem-0.2}/tests/test_physics.py +0 -0
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phaserEM
3
- Version: 0.1
3
+ Version: 0.2
4
4
  Summary: Weapon of choice for electron ptychographic reconstructions
5
5
  Author-email: Colin Gilgenbach <hexane@mit.edu>
6
6
  License-Expression: MPL-2.0
7
7
  Project-URL: Repository, https://github.com/hexane360/phaser.git
8
8
  Project-URL: Documentation, https://hexane360.github.io/phaser/dev
9
+ Keywords: science,atoms,materials,chemistry
9
10
  Classifier: Development Status :: 4 - Beta
10
11
  Classifier: Programming Language :: Python :: 3
11
12
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -13,32 +14,37 @@ Classifier: Programming Language :: Python :: 3.10
13
14
  Classifier: Programming Language :: Python :: 3.11
14
15
  Classifier: Programming Language :: Python :: 3.12
15
16
  Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Intended Audience :: Science/Research
18
+ Classifier: Topic :: Scientific/Engineering :: Physics
16
19
  Classifier: Typing :: Typed
17
20
  Requires-Python: >=3.10
18
21
  Description-Content-Type: text/markdown
19
22
  License-File: LICENSE.txt
20
- Requires-Dist: numpy<2.5,>=1.22
21
- Requires-Dist: scipy<1.18,>=1.7.0
23
+ Requires-Dist: numpy<2.6,>=1.22
24
+ Requires-Dist: scipy<1.19,>=1.7.0
22
25
  Requires-Dist: matplotlib~=3.8
23
26
  Requires-Dist: h5py~=3.8
24
27
  Requires-Dist: pyyaml>=5.3.1
25
28
  Requires-Dist: click~=8.1.0
26
- Requires-Dist: rich~=12.0.0
29
+ Requires-Dist: rich<15,>=12.0.0
27
30
  Requires-Dist: tifffile>=2023.8.25
28
- Requires-Dist: py-pane==0.11.2
31
+ Requires-Dist: py-pane==0.11.3
29
32
  Requires-Dist: typing_extensions~=4.7
30
33
  Provides-Extra: dev
31
34
  Requires-Dist: pytest>=6.2.4; extra == "dev"
32
35
  Requires-Dist: pytest-cov>=3.0.0; extra == "dev"
33
36
  Requires-Dist: coveragepy-lcov>=0.1.1; extra == "dev"
34
- Provides-Extra: cupy
35
- Requires-Dist: cupy-wheel; extra == "cupy"
36
- Requires-Dist: pynvml>=11.0.0; extra == "cupy"
37
+ Provides-Extra: cupy11
38
+ Requires-Dist: cupy-cuda11x>=12.0.0; extra == "cupy11"
39
+ Requires-Dist: pynvml>=11.0.0; extra == "cupy11"
40
+ Provides-Extra: cupy12
41
+ Requires-Dist: cupy-cuda12x>=12.0.0; extra == "cupy12"
42
+ Requires-Dist: pynvml>=11.0.0; extra == "cupy12"
37
43
  Provides-Extra: jax
38
- Requires-Dist: jax<0.6,>=0.4.25; extra == "jax"
44
+ Requires-Dist: jax<0.8,>=0.4.25; extra == "jax"
39
45
  Requires-Dist: optax>=0.2.2; extra == "jax"
40
46
  Provides-Extra: web
41
- Requires-Dist: Quart>=0.19.5; extra == "web"
47
+ Requires-Dist: Quart>=0.20.0; extra == "web"
42
48
  Requires-Dist: backoff==2.2.1; extra == "web"
43
49
  Requires-Dist: requests>=2.31.0; extra == "web"
44
50
  Provides-Extra: docs
@@ -52,9 +58,11 @@ Dynamic: license-file
52
58
 
53
59
  phaser: The weapon of choice for ptychographic reconstructions
54
60
  ---
55
- [![][ci-badge]][ci-url] [![][commit-badge]][commit-url] [![][docs-dev-badge]][docs-dev-url]
61
+ [![][ci-badge]][ci-url] [![][commit-badge]][commit-url] [![][docs-dev-badge]][docs-dev-url] [![][binder-badge]][binder-url]
62
+
56
63
 
57
64
  `phaser` is a fully-featured package for multislice electron ptychography.
65
+ See our [arXiv paper](https://arxiv.org/abs/2505.14372) for more details.
58
66
 
59
67
  ## Features
60
68
 
@@ -64,11 +72,18 @@ phaser: The weapon of choice for ptychographic reconstructions
64
72
  - Multiple incoherent probe modes
65
73
  - Probe position correction
66
74
  - Upsampled (sPIE) and segmented ptychography (work in progress)
67
- - Adaptive propagator correction (work in progress)
75
+ - Adaptive propagator correction (contributed by M Zhu)
76
+
77
+ ## Documentation
78
+
79
+ Documentation on `phaser` can be found here: https://hexane360.github.io/phaser/dev/
80
+
81
+ Documentation is still very much a work in progress, so please feel free to open an issue or email me if you have any questions!
68
82
 
69
83
  ## Installation
70
84
 
71
- To install, first clone the repository from github:
85
+ To install, first clone the repository from github.
86
+ This can be done from GitHub Desktop, or from the git command line:
72
87
 
73
88
  ```sh
74
89
  $ git clone https://github.com/hexane360/phaser
@@ -76,9 +91,29 @@ $ git clone https://github.com/hexane360/phaser
76
91
  $ cd phaser
77
92
  ```
78
93
 
79
- If you're using [`cupy`][cupy] or [`jax`][jax] with a GPU, follow the installation instructions for those packages.
94
+ We recommend using a conda environment or Python virtual environment to keep things clean, although this is not mandatory.
95
+
96
+ `phaser` supports multiple computational backends. The simplest (and slowest) is `numpy`. `cupy` can be used for CUDA-accelerated. `jax` supports CPU and GPU acceleration, and is the only backend which supports the gradient descent engine.
97
+ If you're unsure what engines to use, we recommend installing the `jax` engine.
98
+
99
+ If you're using [`cupy`][cupy] or [`jax`][jax] with a GPU, start by following the installation instructions for those packages.
100
+ Jax can be installed with or without CUDA support, if you're using CUDA make sure you install the correct version. Currently, Jax does not support CUDA on Windows.
80
101
 
81
- Then, install with `pip`:
102
+ Before moving on to installing `phaser`, make sure those packages you've installed work:
103
+ ```sh
104
+ $ python
105
+ >>> import jax
106
+ >>> jax.default_backend()
107
+ 'gpu' # should be 'gpu' on cuda, 'cpu' otherwise
108
+ >>> jax.numpy.array([1, 2, 3, 4]) + 1 # test a basic operation
109
+ Array([2, 3, 4, 5], dtype=int32)
110
+
111
+ >>> import cupy
112
+ >>> cupy.array([1, 2, 3, 4]) + 1
113
+ array([2, 3, 4, 5])
114
+ ```
115
+
116
+ Then, install `phaser` using `pip`:
82
117
 
83
118
  ```sh
84
119
  $ python -m pip install -e .
@@ -87,7 +122,22 @@ $ python -m pip install -e .
87
122
  For the jax or cupy backend, or for the optional webserver, install with the corresponding options:
88
123
 
89
124
  ```sh
90
- $ python -m pip install -e '.[cupy,web]' # or '.[jax,web]'
125
+ $ python -m pip install -e ".[jax,cupy12,web]" # for the 'jax', 'cupy12', and 'web' options
126
+ ```
127
+
128
+ Depending on your command line, you may need to put double quotes around the options (as shown).
129
+
130
+ Here are the supported installation options:
131
+
132
+ - `jax`: For the [`jax`][jax] backend (required for the gradient descent engine)
133
+ - `cupy11`: `cupy` for CUDA toolkit 11.x
134
+ - `cupy12`: `cupy` for CUDA toolkit 12.x
135
+ - `web`: For the web interface
136
+
137
+ For [Optuna](https://optuna.org/) hyperoptimization, install it as well:
138
+
139
+ ```sh
140
+ $ pip install optuna
91
141
  ```
92
142
 
93
143
  ## Running
@@ -100,6 +150,20 @@ To run the webserver, call `phaser serve`. By default, the server serves on http
100
150
 
101
151
  To run a worker, call `phaser worker <url>`, where `url` is the URL of a running job server.
102
152
 
153
+ ## Sample data & Examples
154
+
155
+ Sample data can be downloaded from the following dropbox link: https://www.dropbox.com/scl/fo/txm3k88ubrzvt541v23ir/AL-l_m6VnGlFxzHWZSSc0TA?rlkey=8qxtwnc8cwhpff6jpr5s40y6i&st=x9pbwke0&dl=0
156
+
157
+ Copy the `sample_data` directory into the root code folder.
158
+
159
+ ```sh
160
+ $ curl --output sample_data.zip -L 'https://www.dropbox.com/scl/fo/txm3k88ubrzvt541v23ir/AL-l_m6VnGlFxzHWZSSc0TA?rlkey=8qxtwnc8cwhpff6jpr5s40y6i&st=x9pbwke0&dl=1'
161
+ $ unzip sample_data.zip -x / -d sample_data
162
+ ```
163
+
164
+ Sample data includes simulated and experimental MoS2 data, simulated and experimental Si data, and experimental PrScO3 data.
165
+ After the data is downloaded, any of the example reconstructions can be run as `phaser run examples/mos2_grad.yaml` (for example).
166
+
103
167
  ## Alternatives
104
168
 
105
169
  Other notable ptychography packages:
@@ -108,6 +172,7 @@ Other notable ptychography packages:
108
172
  - [`py4DSTEM`](https://github.com/py4dstem/py4DSTEM)
109
173
  - [`PtyLab.m/py/jl`](https://opg.optica.org/oe/fulltext.cfm?uri=oe-31-9-13763&id=529026)
110
174
  - [PtyPy](https://ptycho.github.io/ptypy/)
175
+ - [PtyRAD](https://github.com/chiahao3/ptyrad)
111
176
 
112
177
  [numpy]: https://numpy.org/
113
178
  [cupy]: https://cupy.dev/
@@ -119,3 +184,5 @@ Other notable ptychography packages:
119
184
  [docs-dev-url]: https://hexane360.github.io/phaser/dev/
120
185
  [commit-badge]: https://img.shields.io/github/last-commit/hexane360/phaser
121
186
  [commit-url]: https://github.com/hexane360/phaser/commits
187
+ [binder-badge]: https://mybinder.org/badge_logo.svg
188
+ [binder-url]: https://mybinder.org/v2/gh/hexane360/phaser/HEAD
phaserem-0.2/README.md ADDED
@@ -0,0 +1,130 @@
1
+ phaser: The weapon of choice for ptychographic reconstructions
2
+ ---
3
+ [![][ci-badge]][ci-url] [![][commit-badge]][commit-url] [![][docs-dev-badge]][docs-dev-url] [![][binder-badge]][binder-url]
4
+
5
+
6
+ `phaser` is a fully-featured package for multislice electron ptychography.
7
+ See our [arXiv paper](https://arxiv.org/abs/2505.14372) for more details.
8
+
9
+ ## Features
10
+
11
+ - ePIE, LSQ-MLs, and gradient descent algorithms
12
+ - [`numpy`][numpy], [`cupy`][cupy], and [`jax`][jax] backends.
13
+ - Single and multislice ptychography
14
+ - Multiple incoherent probe modes
15
+ - Probe position correction
16
+ - Upsampled (sPIE) and segmented ptychography (work in progress)
17
+ - Adaptive propagator correction (contributed by M Zhu)
18
+
19
+ ## Documentation
20
+
21
+ Documentation on `phaser` can be found here: https://hexane360.github.io/phaser/dev/
22
+
23
+ Documentation is still very much a work in progress, so please feel free to open an issue or email me if you have any questions!
24
+
25
+ ## Installation
26
+
27
+ To install, first clone the repository from github.
28
+ This can be done from GitHub Desktop, or from the git command line:
29
+
30
+ ```sh
31
+ $ git clone https://github.com/hexane360/phaser
32
+ # enter phaser directory
33
+ $ cd phaser
34
+ ```
35
+
36
+ We recommend using a conda environment or Python virtual environment to keep things clean, although this is not mandatory.
37
+
38
+ `phaser` supports multiple computational backends. The simplest (and slowest) is `numpy`. `cupy` can be used for CUDA-accelerated. `jax` supports CPU and GPU acceleration, and is the only backend which supports the gradient descent engine.
39
+ If you're unsure what engines to use, we recommend installing the `jax` engine.
40
+
41
+ If you're using [`cupy`][cupy] or [`jax`][jax] with a GPU, start by following the installation instructions for those packages.
42
+ Jax can be installed with or without CUDA support, if you're using CUDA make sure you install the correct version. Currently, Jax does not support CUDA on Windows.
43
+
44
+ Before moving on to installing `phaser`, make sure those packages you've installed work:
45
+ ```sh
46
+ $ python
47
+ >>> import jax
48
+ >>> jax.default_backend()
49
+ 'gpu' # should be 'gpu' on cuda, 'cpu' otherwise
50
+ >>> jax.numpy.array([1, 2, 3, 4]) + 1 # test a basic operation
51
+ Array([2, 3, 4, 5], dtype=int32)
52
+
53
+ >>> import cupy
54
+ >>> cupy.array([1, 2, 3, 4]) + 1
55
+ array([2, 3, 4, 5])
56
+ ```
57
+
58
+ Then, install `phaser` using `pip`:
59
+
60
+ ```sh
61
+ $ python -m pip install -e .
62
+ ```
63
+
64
+ For the jax or cupy backend, or for the optional webserver, install with the corresponding options:
65
+
66
+ ```sh
67
+ $ python -m pip install -e ".[jax,cupy12,web]" # for the 'jax', 'cupy12', and 'web' options
68
+ ```
69
+
70
+ Depending on your command line, you may need to put double quotes around the options (as shown).
71
+
72
+ Here are the supported installation options:
73
+
74
+ - `jax`: For the [`jax`][jax] backend (required for the gradient descent engine)
75
+ - `cupy11`: `cupy` for CUDA toolkit 11.x
76
+ - `cupy12`: `cupy` for CUDA toolkit 12.x
77
+ - `web`: For the web interface
78
+
79
+ For [Optuna](https://optuna.org/) hyperoptimization, install it as well:
80
+
81
+ ```sh
82
+ $ pip install optuna
83
+ ```
84
+
85
+ ## Running
86
+
87
+ After installation, the `phaser` command should be available. Phaser can be run from the command line, or through a job server.
88
+
89
+ To run a single reconstruction on the command line, call `phaser run <file>`, where `file` is the path to a reconstruction plan file.
90
+
91
+ To run the webserver, call `phaser serve`. By default, the server serves on https://localhost:5050/, so navigate there in a web browser. The server interface can be used to start workers and schedule reconstruction jobs.
92
+
93
+ To run a worker, call `phaser worker <url>`, where `url` is the URL of a running job server.
94
+
95
+ ## Sample data & Examples
96
+
97
+ Sample data can be downloaded from the following dropbox link: https://www.dropbox.com/scl/fo/txm3k88ubrzvt541v23ir/AL-l_m6VnGlFxzHWZSSc0TA?rlkey=8qxtwnc8cwhpff6jpr5s40y6i&st=x9pbwke0&dl=0
98
+
99
+ Copy the `sample_data` directory into the root code folder.
100
+
101
+ ```sh
102
+ $ curl --output sample_data.zip -L 'https://www.dropbox.com/scl/fo/txm3k88ubrzvt541v23ir/AL-l_m6VnGlFxzHWZSSc0TA?rlkey=8qxtwnc8cwhpff6jpr5s40y6i&st=x9pbwke0&dl=1'
103
+ $ unzip sample_data.zip -x / -d sample_data
104
+ ```
105
+
106
+ Sample data includes simulated and experimental MoS2 data, simulated and experimental Si data, and experimental PrScO3 data.
107
+ After the data is downloaded, any of the example reconstructions can be run as `phaser run examples/mos2_grad.yaml` (for example).
108
+
109
+ ## Alternatives
110
+
111
+ Other notable ptychography packages:
112
+
113
+ - [`fold_slice`](https://github.com/yijiang1/fold_slice) branch of [PtychoShelves](https://www.psi.ch/en/sls/csaxs/software#coming-soon-ptychoshelves-a-versatile-high-level-framework-for-high-performance-analysis-of)
114
+ - [`py4DSTEM`](https://github.com/py4dstem/py4DSTEM)
115
+ - [`PtyLab.m/py/jl`](https://opg.optica.org/oe/fulltext.cfm?uri=oe-31-9-13763&id=529026)
116
+ - [PtyPy](https://ptycho.github.io/ptypy/)
117
+ - [PtyRAD](https://github.com/chiahao3/ptyrad)
118
+
119
+ [numpy]: https://numpy.org/
120
+ [cupy]: https://cupy.dev/
121
+ [jax]: https://docs.jax.dev/en/latest/
122
+
123
+ [ci-badge]: https://github.com/hexane360/phaser/workflows/Tests/badge.svg
124
+ [ci-url]: https://github.com/hexane360/phaser/actions/workflows/ci.yaml
125
+ [docs-dev-badge]: https://img.shields.io/badge/docs-dev-blue
126
+ [docs-dev-url]: https://hexane360.github.io/phaser/dev/
127
+ [commit-badge]: https://img.shields.io/github/last-commit/hexane360/phaser
128
+ [commit-url]: https://github.com/hexane360/phaser/commits
129
+ [binder-badge]: https://mybinder.org/badge_logo.svg
130
+ [binder-url]: https://mybinder.org/v2/gh/hexane360/phaser/HEAD
@@ -93,12 +93,11 @@ class PoissonNoiseModel(NoiseModel[None]):
93
93
  ) -> t.Tuple[Float, None]:
94
94
  xp = get_array_module(model_wave, model_intensity, exp_patterns, mask)
95
95
  patterns = xp.maximum(exp_patterns, 0.0)
96
- #intensity - patterns * xp.log(intensity + self.offset)
97
96
 
98
97
  loss = xp.sum(mask * (
99
- #model_intensity - patterns * xp.log(model_intensity + self.eps)
100
- model_intensity + patterns * (xp.log(patterns + self.eps) - xp.log(model_intensity + self.eps) - 1.0)
101
- #patterns - (model_intensity + self.offset) * xp.log(patterns)
98
+ model_intensity + self.eps + patterns * (
99
+ xp.log(patterns + self.eps) - xp.log(model_intensity + self.eps) - 1.0
100
+ )
102
101
  )).astype(exp_patterns.dtype)
103
102
  return (loss, state)
104
103
 
@@ -1,9 +1,9 @@
1
1
  from functools import partial
2
+ import logging
2
3
  from pathlib import Path
3
4
  import typing as t
4
5
 
5
6
  import numpy
6
- from numpy.typing import NDArray
7
7
  import tifffile
8
8
 
9
9
  from phaser.utils.num import to_numpy, abs2, fft2, get_array_module
@@ -18,9 +18,10 @@ def output_images(state: ReconsState, out_dir: Path, options: SaveOptions):
18
18
  if ty not in _SAVE_FUNCS:
19
19
  raise ValueError(f"Unknown image type '{ty}'")
20
20
 
21
+ ext = options.plot_ext if ty in _PLOT_FUNCS else 'tiff'
21
22
  try:
22
23
  out_name = options.img_fmt.format(
23
- type=ty, iter=state.iter,
24
+ type=ty, iter=state.iter, ext=ext
24
25
  )
25
26
  out_path = out_dir / out_name
26
27
  except KeyError as e:
@@ -122,7 +123,7 @@ def _save_object_phase(state: ReconsState, out_path: Path, options: SaveOptions,
122
123
  )
123
124
 
124
125
  if crop:
125
- obj_phase = obj_phase[..., *state.object.sampling.get_region_crop()]
126
+ obj_phase = obj_phase[(Ellipsis, *state.object.sampling.get_region_crop())]
126
127
  mask = xp.ones(obj_phase.shape[-2:], dtype=numpy.bool_)
127
128
  else:
128
129
  # include whole image, but only scale based on ROI
@@ -158,7 +159,7 @@ def _save_object_mag(state: ReconsState, out_path: Path, options: SaveOptions, s
158
159
  xp = get_array_module(state.object.data)
159
160
  obj_mag = abs2(state.object.data)
160
161
  if crop:
161
- obj_mag = obj_mag[..., *state.object.sampling.get_region_crop()]
162
+ obj_mag = obj_mag[(Ellipsis, *state.object.sampling.get_region_crop())]
162
163
  mask = numpy.ones(obj_mag.shape[-2:], dtype=numpy.bool_)
163
164
  else:
164
165
  # include whole image, but only scale based on ROI
@@ -176,6 +177,65 @@ def _save_object_mag(state: ReconsState, out_path: Path, options: SaveOptions, s
176
177
  w.write(obj_mag, **write_opts)
177
178
 
178
179
 
180
+ def _plot_scan(state: ReconsState, out_path: Path, options: SaveOptions):
181
+ from matplotlib import pyplot
182
+ fig, ax = pyplot.subplots(figsize=(4, 4), dpi=options.plot_dpi, constrained_layout=True)
183
+
184
+ ax.set_aspect(1.)
185
+ [left, right, bottom, top] = state.object.sampling.mpl_extent()
186
+ ax.set_xlim(left, right)
187
+ ax.set_ylim(bottom, top)
188
+
189
+ scan = to_numpy(state.scan)
190
+ i = numpy.arange(scan[..., 0].size)
191
+ ax.scatter(scan[..., 1].ravel(), scan[..., 0].ravel(), c=i, cmap='plasma', s=0.5, edgecolors='none')
192
+
193
+ fig.savefig(out_path)
194
+ pyplot.close(fig)
195
+
196
+
197
+ def _plot_tilt(state: ReconsState, out_path: Path, options: SaveOptions):
198
+ from matplotlib import pyplot
199
+
200
+ if state.tilt is None:
201
+ logger = logging.getLogger(__name__)
202
+ logger.warning("Tilt map (`state.tilt`) is missing, skipping `plot_tilt`")
203
+ return
204
+
205
+ fig, ax = pyplot.subplots(figsize=(4, 4), dpi=options.plot_dpi, constrained_layout=True)
206
+
207
+ ax.set_aspect(1.)
208
+ [left, right, bottom, top] = state.object.sampling.mpl_extent()
209
+ ax.set_xlim(left, right)
210
+ ax.set_ylim(bottom, top)
211
+
212
+ scan = to_numpy(state.scan)
213
+ tilt = to_numpy(state.tilt)
214
+ tilt = tilt[..., 1] + tilt[..., 0]*1.j
215
+ max_tilt = max(numpy.max(numpy.abs(tilt)), 1.0) # at least 1 mrad
216
+ c = colorize_complex(tilt.ravel() / max_tilt, amp=True, rescale=False)
217
+ ax.scatter(scan[..., 1].ravel(), scan[..., 0].ravel(), c=c, s=0.5, edgecolors='none')
218
+
219
+ fig.draw_without_rendering()
220
+ trans = ax.transAxes + fig.transFigure.inverted()
221
+ legend_ax_max = trans.transform([0.95, 0.02])
222
+ legend_ax_size = (0.1, 0.1)
223
+ legend_ax = fig.add_axes((legend_ax_max[0] - legend_ax_size[0], legend_ax_max[1], *legend_ax_size), projection='polar')
224
+
225
+ legend_ax.set_rmax(max_tilt) # type: ignore
226
+ legend_ax.set_theta_direction(-1) # type: ignore
227
+ legend_ax.set_axis_off()
228
+
229
+ thetas = numpy.linspace(0., 2*numpy.pi, 70)
230
+ rs = numpy.concatenate([[0.0], numpy.geomspace(0.1, 1.0, 30)])
231
+ rr, tt = numpy.meshgrid(rs, thetas, indexing='ij')
232
+ c2 = colorize_complex(rr * numpy.exp(1.j * tt), rescale=False)
233
+ legend_ax.pcolormesh(tt, rr * max_tilt, c2)
234
+ legend_ax.text(-numpy.pi/2., max_tilt * 1.05, f"{max_tilt:.1f} mrad", ha='center', va='bottom', size='small')
235
+
236
+ fig.savefig(out_path)
237
+ pyplot.close(fig)
238
+
179
239
 
180
240
  _SAVE_FUNCS: t.Dict[str, t.Callable[[ReconsState, Path, SaveOptions], t.Any]] = {
181
241
  'probe': _save_probe,
@@ -186,4 +246,8 @@ _SAVE_FUNCS: t.Dict[str, t.Callable[[ReconsState, Path, SaveOptions], t.Any]] =
186
246
  'object_phase_sum': partial(_save_object_phase, stack=False),
187
247
  'object_mag_stack': partial(_save_object_mag, stack=True),
188
248
  'object_mag_sum': partial(_save_object_mag, stack=False),
189
- }
249
+ 'scan': _plot_scan,
250
+ 'tilt': _plot_tilt,
251
+ }
252
+ # save functions with special handling of file extensions
253
+ _PLOT_FUNCS: t.Set[str] = {'scan', 'tilt'}
@@ -6,14 +6,14 @@ import numpy
6
6
  from numpy.typing import NDArray
7
7
 
8
8
  from phaser.utils.num import (
9
- get_array_module, get_scipy_module, Float,
10
- jit, fft2, ifft2, abs2, xp_is_jax, to_real_dtype
9
+ get_array_module, get_scipy_module, Float, unstack,
10
+ jit, fft2, ifft2, abs2, xp_is_jax, to_real_dtype, to_numpy
11
11
  )
12
12
  from phaser.state import ReconsState
13
13
  from phaser.hooks.regularization import (
14
14
  ClampObjectAmplitudeProps, LimitProbeSupportProps,
15
- RegularizeLayersProps, ObjLowPassProps,
16
- CostRegularizerProps, TVRegularizerProps
15
+ RegularizeLayersProps, ObjLowPassProps, GaussianProps,
16
+ CostRegularizerProps, TVRegularizerProps, UnstructuredGaussianProps
17
17
  )
18
18
 
19
19
 
@@ -161,6 +161,37 @@ class ObjLowPass:
161
161
  return (sim, state)
162
162
 
163
163
 
164
+ class ObjGaussian:
165
+ def __init__(self, args: None, props: GaussianProps):
166
+ self.sigma = props.sigma
167
+ self.weight = props.weight
168
+
169
+ def init_state(self, sim: ReconsState) -> NDArray[numpy.floating]:
170
+ samp = sim.object.sampling
171
+ xp = get_array_module(sim.object.data)
172
+ dtype = to_real_dtype(sim.object.data.dtype)
173
+
174
+ ky = xp.fft.fftfreq(samp.shape[0], samp.sampling[0]).astype(dtype)
175
+ kx = xp.fft.fftfreq(samp.shape[1], samp.sampling[1]).astype(dtype)
176
+ (ky, kx) = xp.meshgrid(ky, kx, indexing='ij')
177
+ k2 = ky**2 + kx**2
178
+
179
+ filt = xp.exp(-(2 * numpy.pi**2 * self.sigma**2) * k2).astype(dtype)
180
+ return (1. - self.weight * (1. - filt)).astype(dtype)
181
+
182
+ def apply_group(
183
+ self, group: NDArray[numpy.integer], sim: ReconsState, state: NDArray[numpy.floating]
184
+ ) -> t.Tuple[ReconsState, NDArray[numpy.floating]]:
185
+ return self.apply_iter(sim, state)
186
+
187
+ def apply_iter(
188
+ self, sim: ReconsState, state: NDArray[numpy.floating]
189
+ ) -> t.Tuple[ReconsState, NDArray[numpy.floating]]:
190
+ # TODO: should this be done in-place?
191
+ sim.object.data = ifft2(state * fft2(sim.object.data))
192
+ return (sim, state)
193
+
194
+
164
195
  class ObjL1:
165
196
  def __init__(self, args: None, props: CostRegularizerProps):
166
197
  self.cost: float = props.cost
@@ -395,6 +426,77 @@ class ProbeRecipTotalVariation:
395
426
  return (cost * cost_scale * self.cost, state)
396
427
 
397
428
 
429
+ class UnstructuredGaussian:
430
+ def __init__(self, args: None, props: UnstructuredGaussianProps):
431
+ self.weight = props.weight
432
+ self.sigma = props.sigma
433
+ self.attr_path = props.attr_path
434
+
435
+ def init_state(self, sim: ReconsState) -> NDArray[numpy.floating]:
436
+ xp = get_array_module(sim.scan)
437
+ try:
438
+ self.getattr_nested(sim, self.attr_path)
439
+ except AttributeError as e:
440
+ raise AttributeError(f"Can't get path '{self.attr_path}' in reconstruction state") from e
441
+
442
+ # precompute Gaussian filter
443
+ obj_samp = sim.object.sampling
444
+ ky = xp.fft.fftfreq(obj_samp.shape[0], obj_samp.sampling[0])
445
+ kx = xp.fft.fftfreq(obj_samp.shape[1], obj_samp.sampling[1])
446
+ ky, kx = xp.meshgrid(ky, kx, indexing='ij')
447
+ k2 = ky**2 + kx**2
448
+ return xp.exp(- (2 * numpy.pi**2 * self.sigma**2) * k2)
449
+
450
+ def getattr_nested(self, obj: t.Any, attr_path: str) -> t.Any:
451
+ for attr in attr_path.split('.'):
452
+ obj = getattr(obj, attr)
453
+ return obj
454
+
455
+ def setattr_nested(self, obj: t.Any, attr_path: str, value: t.Any):
456
+ *parents, last = attr_path.split('.')
457
+ for attr in parents:
458
+ obj = getattr(obj, attr)
459
+ setattr(obj, last, value)
460
+
461
+ def apply_iter(self, sim: ReconsState, state: NDArray[numpy.floating]) -> t.Tuple[ReconsState, NDArray[numpy.floating]]:
462
+ from scipy.spatial import KDTree
463
+ obj_samp = sim.object.sampling
464
+ scan_flat = sim.scan.reshape(-1, 2)
465
+ scan_ndim = sim.scan.ndim - 1
466
+
467
+ attr = self.getattr_nested(sim, self.attr_path)
468
+ vals = t.cast(NDArray[numpy.inexact], getattr(attr, 'data', attr)) # Extract raw array
469
+ xp = get_array_module(vals)
470
+
471
+ is_complex = xp.iscomplexobj(vals)
472
+
473
+ if is_complex:
474
+ vals = xp.stack([vals.real, vals.imag], axis=-1)
475
+
476
+ tree = KDTree(to_numpy(scan_flat))
477
+ obj_pts = numpy.stack(obj_samp.grid(xp=numpy), axis=-1)
478
+ # val_img should be on GPU
479
+ val_img = vals.reshape((-1, *vals.shape[scan_ndim:]))[xp.array(tree.query(obj_pts)[1])]
480
+
481
+ # blur in Fourier space
482
+ # HACK: the transposes are so we FFT over the first two axes
483
+ val_img_blur = ifft2(fft2(val_img.T) * state.T).real.T
484
+
485
+ # then sample back to the scan positions
486
+ idxs = xp.round((scan_flat - obj_samp.corner) / obj_samp.sampling).astype(numpy.int_)
487
+ blur_vals = val_img_blur[unstack(idxs, axis=-1)].reshape(vals.shape)
488
+
489
+ new_vals = self.weight * blur_vals + (1 - self.weight) * vals
490
+
491
+ if is_complex:
492
+ new_vals = new_vals[..., 0] + new_vals[..., 1] * 1.j
493
+
494
+ # TODO: this should be expressed as immutable
495
+ self.setattr_nested(sim, self.attr_path, new_vals)
496
+
497
+ return (sim, state)
498
+
499
+
398
500
  def img_grad(img: numpy.ndarray) -> t.Tuple[numpy.ndarray, numpy.ndarray]:
399
501
  xp = get_array_module(img)
400
502
  return (
@@ -8,7 +8,7 @@ from typing_extensions import Self
8
8
 
9
9
  from phaser.utils.num import (
10
10
  get_array_module, to_real_dtype, to_complex_dtype,
11
- fft2, ifft2, is_jax, to_numpy, block_until_ready,
11
+ fft2, ifft2, is_jax, to_numpy, block_until_ready, ufunc_outer
12
12
  )
13
13
  from phaser.utils.misc import FloatKey, jax_dataclass, create_compact_groupings, create_sparse_groupings, shuffled
14
14
  from phaser.utils.optics import fresnel_propagator, fourier_shift_filter
@@ -190,6 +190,38 @@ def make_propagators(state: ReconsState, bwlim_frac: t.Optional[float] = 2/3) ->
190
190
  )
191
191
 
192
192
 
193
+ def tilt_propagators(
194
+ ky: NDArray[numpy.floating], kx: NDArray[numpy.floating],
195
+ state: ReconsState,
196
+ props: t.Optional[NDArray[numpy.complexfloating]], # shape: (Nz-1, Ny, Nx)
197
+ tilts: t.Optional[NDArray[numpy.floating]] # shape: (..., 2), in mrad
198
+ ) -> t.Optional[NDArray[numpy.complexfloating]]:
199
+ """
200
+ Applies tilt and slice-dependent propagation phase shifts to props.
201
+ -------
202
+ NDArray[complex] or None
203
+ Tilted propagators of shape (n_layers-1, ..., Ny, Nx), or None if no slices.
204
+ """
205
+ if props is None:
206
+ return None
207
+ if tilts is None:
208
+ return props[:, None, ...]
209
+
210
+ xp = get_array_module(state.probe.data)
211
+ dtype = to_real_dtype(state.probe.data.dtype)
212
+ complex_dtype = to_complex_dtype(dtype)
213
+ delta_zs = state.object.thicknesses[:-1]
214
+
215
+ tilt_ramps = xp.exp( # (n_layers-1, batch, Ny, Nx)
216
+ 2.j * xp.pi * ufunc_outer(xp.multiply, delta_zs, (
217
+ ufunc_outer(xp.multiply, xp.tan(tilts[..., 0] * 1e-3), ky) +
218
+ ufunc_outer(xp.multiply, xp.tan(tilts[..., 1] * 1e-3), kx)
219
+ ))
220
+ )
221
+
222
+ return props[(slice(None), *(None,)*(tilts.ndim - 1), Ellipsis)] * tilt_ramps.astype(complex_dtype)
223
+
224
+
193
225
  @t.overload
194
226
  def cutout_group(
195
227
  ky: NDArray[numpy.floating], kx: NDArray[numpy.floating],
@@ -236,16 +268,20 @@ def slice_forwards(
236
268
  if props is None:
237
269
  return f(0, None, state)
238
270
 
239
- n_slices = len(props) + 1
271
+ n_slices = len(props) + 1 # props shape: (Nz-1, batch, Ny, Nx)
240
272
 
241
273
  if is_jax(props):
242
274
  import jax
243
- state = jax.lax.fori_loop(0, n_slices - 1, lambda slice_i, state: f(slice_i, props[slice_i], state), state, unroll=False)
275
+ def step_fn(carry, slice_i):
276
+ new_state = f(slice_i, props[slice_i], carry)
277
+ return new_state, None
278
+
279
+ state, _ = jax.lax.scan(step_fn, state, jax.numpy.arange(n_slices - 1))
244
280
  return f(n_slices - 1, None, state)
245
281
 
282
+ # fallback numpy mode
246
283
  for slice_i in range(n_slices - 1):
247
284
  state = f(slice_i, props[slice_i], state)
248
-
249
285
  return f(n_slices - 1, None, state)
250
286
 
251
287
 
@@ -267,4 +303,4 @@ def slice_backwards(
267
303
  for slice_i in range(n_slices - 1, 0, -1):
268
304
  state = f(slice_i, props[slice_i - 1], state)
269
305
 
270
- return f(0, None, state)
306
+ return f(0, None, state)