phaserEM 0.2__tar.gz → 0.3.0__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.
- {phaserem-0.2 → phaserem-0.3.0}/PKG-INFO +17 -4
- phaserem-0.3.0/phaser/__init__.py +1 -0
- {phaserem-0.2 → phaserem-0.3.0}/phaser/__main__.py +1 -1
- phaserem-0.3.0/phaser/cli/__init__.py +161 -0
- phaserem-0.3.0/phaser/cli/calc_drift.py +424 -0
- phaserem-0.3.0/phaser/cli/process_empad.py +308 -0
- phaserem-0.3.0/phaser/cli/validate.py +51 -0
- {phaserem-0.2 → phaserem-0.3.0}/phaser/engines/common/noise_models.py +14 -6
- {phaserem-0.2 → phaserem-0.3.0}/phaser/engines/common/output.py +4 -4
- {phaserem-0.2 → phaserem-0.3.0}/phaser/engines/common/position_correction.py +2 -2
- {phaserem-0.2 → phaserem-0.3.0}/phaser/engines/common/regularizers.py +122 -36
- {phaserem-0.2 → phaserem-0.3.0}/phaser/engines/common/simulation.py +73 -21
- {phaserem-0.2 → phaserem-0.3.0}/phaser/engines/conventional/run.py +53 -30
- {phaserem-0.2 → phaserem-0.3.0}/phaser/engines/conventional/solvers.py +79 -31
- phaserem-0.3.0/phaser/engines/gradient/run.py +617 -0
- phaserem-0.3.0/phaser/engines/gradient/solvers.py +307 -0
- phaserem-0.3.0/phaser/engines/gradient/synthetic.py +125 -0
- {phaserem-0.2 → phaserem-0.3.0}/phaser/execute.py +77 -40
- {phaserem-0.2 → phaserem-0.3.0}/phaser/hooks/__init__.py +90 -48
- phaserem-0.3.0/phaser/hooks/_dependencies.py +46 -0
- phaserem-0.3.0/phaser/hooks/filter.py +70 -0
- {phaserem-0.2 → phaserem-0.3.0}/phaser/hooks/hook.py +17 -6
- {phaserem-0.2 → phaserem-0.3.0}/phaser/hooks/io/empad.py +2 -2
- phaserem-0.3.0/phaser/hooks/io/gatan.py +98 -0
- {phaserem-0.2 → phaserem-0.3.0}/phaser/hooks/io/manual.py +1 -0
- phaserem-0.3.0/phaser/hooks/io/nion.py +116 -0
- {phaserem-0.2 → phaserem-0.3.0}/phaser/hooks/object.py +4 -3
- {phaserem-0.2 → phaserem-0.3.0}/phaser/hooks/preprocessing.py +98 -28
- {phaserem-0.2 → phaserem-0.3.0}/phaser/hooks/probe.py +9 -5
- {phaserem-0.2 → phaserem-0.3.0}/phaser/hooks/regularization.py +16 -6
- phaserem-0.3.0/phaser/hooks/scan.py +48 -0
- {phaserem-0.2 → phaserem-0.3.0}/phaser/hooks/schedule.py +4 -2
- {phaserem-0.2 → phaserem-0.3.0}/phaser/hooks/solver.py +15 -7
- {phaserem-0.2 → phaserem-0.3.0}/phaser/io/empad.py +80 -1
- phaserem-0.3.0/phaser/io/gatan.py +298 -0
- phaserem-0.3.0/phaser/io/nion.py +107 -0
- {phaserem-0.2 → phaserem-0.3.0}/phaser/main.py +29 -9
- {phaserem-0.2 → phaserem-0.3.0}/phaser/observer.py +40 -11
- {phaserem-0.2 → phaserem-0.3.0}/phaser/plan.py +59 -9
- {phaserem-0.2 → phaserem-0.3.0}/phaser/state.py +112 -74
- {phaserem-0.2 → phaserem-0.3.0}/phaser/types.py +106 -15
- {phaserem-0.2 → phaserem-0.3.0}/phaser/utils/_cuda_kernels.py +34 -0
- phaserem-0.3.0/phaser/utils/_jax_kernels.py +231 -0
- phaserem-0.3.0/phaser/utils/_torch_kernels.py +683 -0
- {phaserem-0.2 → phaserem-0.3.0}/phaser/utils/analysis.py +6 -1
- phaserem-0.3.0/phaser/utils/config.py +188 -0
- phaserem-0.3.0/phaser/utils/image.py +1581 -0
- phaserem-0.3.0/phaser/utils/io.py +611 -0
- phaserem-0.3.0/phaser/utils/misc.py +529 -0
- {phaserem-0.2 → phaserem-0.3.0}/phaser/utils/num.py +493 -154
- {phaserem-0.2 → phaserem-0.3.0}/phaser/utils/object.py +62 -22
- {phaserem-0.2 → phaserem-0.3.0}/phaser/utils/optics.py +35 -9
- {phaserem-0.2 → phaserem-0.3.0}/phaser/utils/physics.py +6 -1
- phaserem-0.3.0/phaser/utils/scan.py +64 -0
- phaserem-0.3.0/phaser/utils/tree.py +466 -0
- phaserem-0.3.0/phaser/version.py +67 -0
- phaserem-0.3.0/phaser/web/debug.py +140 -0
- phaserem-0.3.0/phaser/web/pubsub.py +320 -0
- phaserem-0.3.0/phaser/web/routes.py +381 -0
- phaserem-0.3.0/phaser/web/server.py +949 -0
- phaserem-0.3.0/phaser/web/static/apple-touch-icon.png +3 -0
- phaserem-0.3.0/phaser/web/static/dist/dashboard.js +3 -0
- phaserem-0.3.0/phaser/web/static/dist/dashboard.js.LICENSE.txt +6 -0
- phaserem-0.3.0/phaser/web/static/dist/dashboard.js.map +1 -0
- phaserem-0.3.0/phaser/web/static/dist/error.js +2 -0
- phaserem-0.3.0/phaser/web/static/dist/error.js.map +1 -0
- phaserem-0.3.0/phaser/web/static/dist/manager.js +3 -0
- phaserem-0.3.0/phaser/web/static/dist/manager.js.LICENSE.txt +6 -0
- phaserem-0.3.0/phaser/web/static/dist/manager.js.map +1 -0
- phaserem-0.3.0/phaser/web/static/dist/runtime.js +2 -0
- phaserem-0.3.0/phaser/web/static/dist/runtime.js.map +1 -0
- phaserem-0.3.0/phaser/web/static/dist/shared.js +3 -0
- phaserem-0.3.0/phaser/web/static/dist/shared.js.LICENSE.txt +58 -0
- phaserem-0.3.0/phaser/web/static/dist/shared.js.map +1 -0
- phaserem-0.3.0/phaser/web/static/favicon.ico +0 -0
- phaserem-0.3.0/phaser/web/static/icon.svg +27 -0
- phaserem-0.3.0/phaser/web/static/logo-text.svg +29 -0
- phaserem-0.3.0/phaser/web/static/logo.svg +21 -0
- phaserem-0.3.0/phaser/web/templates/base.html +19 -0
- {phaserem-0.2 → phaserem-0.3.0}/phaser/web/templates/dashboard.html +2 -2
- phaserem-0.3.0/phaser/web/templates/error.html +19 -0
- {phaserem-0.2 → phaserem-0.3.0}/phaser/web/templates/manager.html +2 -2
- {phaserem-0.2 → phaserem-0.3.0}/phaser/web/types.py +115 -71
- {phaserem-0.2 → phaserem-0.3.0}/phaser/web/util.py +50 -21
- phaserem-0.3.0/phaser/web/views.py +209 -0
- {phaserem-0.2 → phaserem-0.3.0}/phaser/web/worker.py +65 -19
- {phaserem-0.2 → phaserem-0.3.0}/phaserEM.egg-info/PKG-INFO +17 -4
- {phaserem-0.2 → phaserem-0.3.0}/phaserEM.egg-info/SOURCES.txt +40 -5
- phaserem-0.3.0/phaserEM.egg-info/entry_points.txt +2 -0
- {phaserem-0.2 → phaserem-0.3.0}/phaserEM.egg-info/requires.txt +18 -3
- {phaserem-0.2 → phaserem-0.3.0}/pyproject.toml +56 -10
- phaserem-0.3.0/tests/test_engines.py +164 -0
- phaserem-0.3.0/tests/test_image.py +909 -0
- phaserem-0.3.0/tests/test_initialization.py +391 -0
- {phaserem-0.2 → phaserem-0.3.0}/tests/test_load.py +6 -6
- {phaserem-0.2 → phaserem-0.3.0}/tests/test_misc.py +5 -5
- phaserem-0.3.0/tests/test_num.py +366 -0
- {phaserem-0.2 → phaserem-0.3.0}/tests/test_object.py +39 -23
- phaserem-0.3.0/tests/test_optics.py +135 -0
- {phaserem-0.2 → phaserem-0.3.0}/tests/test_physics.py +8 -1
- phaserem-0.3.0/tests/test_state.py +255 -0
- phaserem-0.2/phaser/engines/gradient/run.py +0 -438
- phaserem-0.2/phaser/engines/gradient/solvers.py +0 -139
- phaserem-0.2/phaser/hooks/scan.py +0 -28
- phaserem-0.2/phaser/utils/_jax_kernels.py +0 -102
- phaserem-0.2/phaser/utils/image.py +0 -231
- phaserem-0.2/phaser/utils/io.py +0 -406
- phaserem-0.2/phaser/utils/misc.py +0 -295
- phaserem-0.2/phaser/utils/scan.py +0 -60
- phaserem-0.2/phaser/web/__init__.py +0 -0
- phaserem-0.2/phaser/web/dist/9573273f862f4f5d9644.module.wasm +0 -0
- phaserem-0.2/phaser/web/dist/bundle-dashboard.js +0 -3577
- phaserem-0.2/phaser/web/dist/bundle-manager.js +0 -3455
- phaserem-0.2/phaser/web/dist/bundle-vendors-node_modules_wasm-array_wasm_array_js.js +0 -106
- phaserem-0.2/phaser/web/routes.py +0 -228
- phaserem-0.2/phaser/web/server.py +0 -605
- phaserem-0.2/phaser/web/templates/base.html +0 -13
- phaserem-0.2/phaserEM.egg-info/entry_points.txt +0 -2
- phaserem-0.2/tests/test_image.py +0 -76
- phaserem-0.2/tests/test_initialization.py +0 -184
- phaserem-0.2/tests/test_num.py +0 -213
- phaserem-0.2/tests/test_optics.py +0 -58
- {phaserem-0.2 → phaserem-0.3.0}/LICENSE.txt +0 -0
- {phaserem-0.2 → phaserem-0.3.0}/README.md +0 -0
- {phaserem-0.2/phaser → phaserem-0.3.0/phaser/engines/common}/__init__.py +0 -0
- {phaserem-0.2/phaser/engines/common → phaserem-0.3.0/phaser/engines/conventional}/__init__.py +0 -0
- {phaserem-0.2 → phaserem-0.3.0}/phaser/hooks/tilt.py +0 -0
- {phaserem-0.2/phaser/engines/conventional → phaserem-0.3.0/phaser/io}/__init__.py +0 -0
- {phaserem-0.2 → phaserem-0.3.0}/phaser/py.typed +0 -0
- {phaserem-0.2/phaser/io → phaserem-0.3.0/phaser/utils}/__init__.py +0 -0
- {phaserem-0.2 → phaserem-0.3.0}/phaser/utils/plotting.py +0 -0
- {phaserem-0.2/phaser/utils → phaserem-0.3.0/phaser/web}/__init__.py +0 -0
- {phaserem-0.2 → phaserem-0.3.0}/phaser/web/notebook.py +0 -0
- {phaserem-0.2 → phaserem-0.3.0}/phaser/web/slurm.py +0 -0
- {phaserem-0.2 → phaserem-0.3.0}/phaserEM.egg-info/dependency_links.txt +0 -0
- {phaserem-0.2 → phaserem-0.3.0}/phaserEM.egg-info/top_level.txt +0 -0
- {phaserem-0.2 → phaserem-0.3.0}/setup.cfg +0 -0
- {phaserem-0.2 → phaserem-0.3.0}/setup.py +0 -0
- {phaserem-0.2 → phaserem-0.3.0}/tests/test_empad.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: phaserEM
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
|
@@ -20,16 +20,21 @@ Classifier: Typing :: Typed
|
|
|
20
20
|
Requires-Python: >=3.10
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE.txt
|
|
23
|
-
Requires-Dist: numpy<2.
|
|
23
|
+
Requires-Dist: numpy<2.7,>=2.0
|
|
24
24
|
Requires-Dist: scipy<1.19,>=1.7.0
|
|
25
25
|
Requires-Dist: matplotlib~=3.8
|
|
26
26
|
Requires-Dist: h5py~=3.8
|
|
27
27
|
Requires-Dist: pyyaml>=5.3.1
|
|
28
28
|
Requires-Dist: click~=8.1.0
|
|
29
|
+
Requires-Dist: frozendict~=2.4.7
|
|
29
30
|
Requires-Dist: rich<15,>=12.0.0
|
|
30
31
|
Requires-Dist: tifffile>=2023.8.25
|
|
31
|
-
Requires-Dist:
|
|
32
|
+
Requires-Dist: optree>=0.13.0
|
|
33
|
+
Requires-Dist: py-pane==0.11.7
|
|
32
34
|
Requires-Dist: typing_extensions~=4.7
|
|
35
|
+
Requires-Dist: platformdirs>=4.8.0
|
|
36
|
+
Requires-Dist: PyYAML>=6.0
|
|
37
|
+
Requires-Dist: lxml>=5.0.0
|
|
33
38
|
Provides-Extra: dev
|
|
34
39
|
Requires-Dist: pytest>=6.2.4; extra == "dev"
|
|
35
40
|
Requires-Dist: pytest-cov>=3.0.0; extra == "dev"
|
|
@@ -40,13 +45,21 @@ Requires-Dist: pynvml>=11.0.0; extra == "cupy11"
|
|
|
40
45
|
Provides-Extra: cupy12
|
|
41
46
|
Requires-Dist: cupy-cuda12x>=12.0.0; extra == "cupy12"
|
|
42
47
|
Requires-Dist: pynvml>=11.0.0; extra == "cupy12"
|
|
48
|
+
Provides-Extra: cupy13
|
|
49
|
+
Requires-Dist: cupy-cuda13x>=12.0.0; extra == "cupy13"
|
|
50
|
+
Requires-Dist: pynvml>=11.0.0; extra == "cupy13"
|
|
43
51
|
Provides-Extra: jax
|
|
44
|
-
Requires-Dist: jax<0.
|
|
52
|
+
Requires-Dist: jax<0.11,>=0.4.25; extra == "jax"
|
|
45
53
|
Requires-Dist: optax>=0.2.2; extra == "jax"
|
|
54
|
+
Provides-Extra: torch
|
|
55
|
+
Requires-Dist: torch>=2.8.0; extra == "torch"
|
|
46
56
|
Provides-Extra: web
|
|
47
57
|
Requires-Dist: Quart>=0.20.0; extra == "web"
|
|
48
58
|
Requires-Dist: backoff==2.2.1; extra == "web"
|
|
49
59
|
Requires-Dist: requests>=2.31.0; extra == "web"
|
|
60
|
+
Requires-Dist: anywidget>=0.11.0; extra == "web"
|
|
61
|
+
Requires-Dist: jupyter-ws-tunnel==0.1.3; extra == "web"
|
|
62
|
+
Requires-Dist: hypercorn==0.18.0; extra == "web"
|
|
50
63
|
Provides-Extra: docs
|
|
51
64
|
Requires-Dist: mkdocs==1.6.1; extra == "docs"
|
|
52
65
|
Requires-Dist: mkdocs-material==9.6.11; extra == "docs"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.3.0'
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import importlib
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import sys
|
|
5
|
+
import typing as t
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
class Dependency(abc.ABC):
|
|
10
|
+
@abc.abstractmethod
|
|
11
|
+
def check(self):
|
|
12
|
+
...
|
|
13
|
+
|
|
14
|
+
@abc.abstractmethod
|
|
15
|
+
def install_instructions(self) -> str:
|
|
16
|
+
...
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ImportDependency(Dependency):
|
|
20
|
+
def __init__(self, ref: str, install: str) -> None:
|
|
21
|
+
self.ref = ref
|
|
22
|
+
self.install = install
|
|
23
|
+
|
|
24
|
+
def check(self):
|
|
25
|
+
importlib.import_module(self.ref)
|
|
26
|
+
|
|
27
|
+
def install_instructions(self) -> str:
|
|
28
|
+
return self.install
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def check_dependencies(command: str, dependencies: t.Sequence[str]):
|
|
32
|
+
for dependency in dependencies:
|
|
33
|
+
if (dep := _DEPENDENCIES.get(dependency)) is None:
|
|
34
|
+
raise RuntimeError(f"Unknown dependency '{dependency}'. This is likely a bug in the hook declaration.")
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
dep.check()
|
|
38
|
+
except Exception as e:
|
|
39
|
+
raise RuntimeError(
|
|
40
|
+
f"Missing dependency '{dependency}' required for subcommand '{command}'.\n"
|
|
41
|
+
f"To install: {dep.install_instructions()}"
|
|
42
|
+
) from e
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class MainCommand(click.MultiCommand):
|
|
46
|
+
def __init__(self, commands: t.Iterable[
|
|
47
|
+
t.Union[click.Command, t.Tuple[str, str], t.Tuple[str, str, t.Sequence[str]]]
|
|
48
|
+
], **kwargs):
|
|
49
|
+
super().__init__(**kwargs)
|
|
50
|
+
# name: command or short_help
|
|
51
|
+
self.commands: t.Dict[str, t.Union[click.Command, str, None]] = {}
|
|
52
|
+
self.dependencies: t.Dict[str, t.Sequence[str]] = {}
|
|
53
|
+
for c in commands:
|
|
54
|
+
if isinstance(c, click.Command):
|
|
55
|
+
name = str(c.name)
|
|
56
|
+
self.commands[name] = c
|
|
57
|
+
self.dependencies[name] = ()
|
|
58
|
+
continue
|
|
59
|
+
self.commands[c[0]] = c[1]
|
|
60
|
+
if len(c) > 2:
|
|
61
|
+
self.dependencies[c[0]] = c[2]
|
|
62
|
+
|
|
63
|
+
def list_commands(self, ctx: click.Context):
|
|
64
|
+
return list(self.commands.keys())
|
|
65
|
+
|
|
66
|
+
def format_commands(self, ctx: click.Context, formatter: click.HelpFormatter) -> None:
|
|
67
|
+
from gettext import gettext
|
|
68
|
+
|
|
69
|
+
if len(self.commands):
|
|
70
|
+
limit = formatter.width - 6 - max(map(len, self.commands.keys()))
|
|
71
|
+
|
|
72
|
+
rows = []
|
|
73
|
+
for name, cmd in self.commands.items():
|
|
74
|
+
help = cmd.get_short_help_str(limit) if isinstance(cmd, click.Command) else cmd
|
|
75
|
+
rows.append((name, help))
|
|
76
|
+
|
|
77
|
+
if rows:
|
|
78
|
+
with formatter.section(gettext("Commands")):
|
|
79
|
+
formatter.write_dl(rows)
|
|
80
|
+
|
|
81
|
+
def get_command(self, ctx: click.Context, cmd_name: str) -> t.Optional[click.Command]:
|
|
82
|
+
name = cmd_name.lower()
|
|
83
|
+
val = (self.commands.get(name) or
|
|
84
|
+
self.commands.get(name.replace('-', '_')))
|
|
85
|
+
if val is None:
|
|
86
|
+
return None
|
|
87
|
+
if isinstance(val, click.BaseCommand):
|
|
88
|
+
return val
|
|
89
|
+
|
|
90
|
+
check_dependencies(name, self.dependencies.get(name, ()))
|
|
91
|
+
|
|
92
|
+
# for now, assume `validate` command is `cli.validate:validate`
|
|
93
|
+
module = func = name
|
|
94
|
+
mod = __import__(module, globals(), fromlist=[func], level=1)
|
|
95
|
+
return getattr(mod, func)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@click.command()
|
|
99
|
+
@click.argument('path', type=click.Path(exists=True, dir_okay=False))
|
|
100
|
+
def run(path: t.Union[str, Path]):
|
|
101
|
+
"""Execute a reconstruction plan"""
|
|
102
|
+
from phaser.plan import ReconsPlan
|
|
103
|
+
from phaser.execute import execute_plan
|
|
104
|
+
plans = ReconsPlan.from_yaml_all(path)
|
|
105
|
+
|
|
106
|
+
for plan in plans:
|
|
107
|
+
execute_plan(plan)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@click.command()
|
|
111
|
+
@click.option('--host', type=str, default='localhost', help="Host to serve on")
|
|
112
|
+
@click.option('--port', type=int, help="Port to serve on")
|
|
113
|
+
@click.option('-v', '--verbose', count=True, help="Increase verbosity")
|
|
114
|
+
@click.option('--debug', is_flag=True, help="Enable debug routes (/debug/*), for testing the web interface")
|
|
115
|
+
def serve(host: str = 'localhost', port: t.Optional[int] = None, verbose: int = 0, debug: bool = False):
|
|
116
|
+
"""Run phaser server"""
|
|
117
|
+
from phaser.web.server import server
|
|
118
|
+
|
|
119
|
+
if ':' in host:
|
|
120
|
+
(host, port_from_host) = host.rsplit(':', maxsplit=1)
|
|
121
|
+
try:
|
|
122
|
+
port_from_host = int(port_from_host)
|
|
123
|
+
except ValueError:
|
|
124
|
+
print(f"Invalid host '{host}:{port_from_host}'", file=sys.stderr)
|
|
125
|
+
sys.exit(1)
|
|
126
|
+
|
|
127
|
+
port = port or port_from_host
|
|
128
|
+
|
|
129
|
+
server.run(hostname=host, port=port, verbosity=verbose, debug=debug)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@click.command()
|
|
133
|
+
@click.argument('url', type=str, required=True)
|
|
134
|
+
@click.option('--quiet/--loud', default=False, help="Whether to print output to stdout")
|
|
135
|
+
def worker(url: str, quiet: bool = False):
|
|
136
|
+
"""
|
|
137
|
+
Run phaser worker.
|
|
138
|
+
|
|
139
|
+
URL is the server URL to connect to.
|
|
140
|
+
"""
|
|
141
|
+
from phaser.web.worker import run_worker
|
|
142
|
+
|
|
143
|
+
run_worker(url, quiet=quiet)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
_DEPENDENCIES = {
|
|
147
|
+
'rsciio': ImportDependency('rsciio', "'pip install rosettasciio' or 'conda install rosettasciio'"),
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
commands: t.List[t.Union[click.Command, t.Union[t.Tuple[str, str, t.Sequence[str]], t.Tuple[str, str]]]] = [
|
|
151
|
+
run, serve, worker,
|
|
152
|
+
# these will be looked up in the cli folder
|
|
153
|
+
('validate', "Validate reconstruction plan file"),
|
|
154
|
+
('process_empad', "Process EMPAD XML metadata"),
|
|
155
|
+
('calc_drift', "Calculate and correct linear drift"),
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@click.command(cls=MainCommand, commands=commands)
|
|
160
|
+
def cli():
|
|
161
|
+
pass
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from queue import Queue, Empty
|
|
3
|
+
from threading import Thread, Event
|
|
4
|
+
import fnmatch
|
|
5
|
+
import typing as t
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import numpy
|
|
9
|
+
from numpy.typing import NDArray, ArrayLike
|
|
10
|
+
import scipy.ndimage
|
|
11
|
+
import json
|
|
12
|
+
import h5py
|
|
13
|
+
from matplotlib import pyplot
|
|
14
|
+
from matplotlib.patches import Circle, PathPatch
|
|
15
|
+
from matplotlib.path import Path as MplPath
|
|
16
|
+
from matplotlib.backend_bases import MouseEvent, MouseButton, PickEvent, KeyEvent
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.prompt import Prompt, FloatPrompt, Confirm
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# TODO: generalize to other detectors
|
|
22
|
+
from phaser.io.empad import EmpadMetadata, load_4d
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_adf(path: t.Union[str, Path]) -> t.Tuple[numpy.ndarray, t.Any]:
|
|
26
|
+
f = h5py.File(path)
|
|
27
|
+
|
|
28
|
+
images = t.cast(h5py.Group, f['Data/Image'])
|
|
29
|
+
if len(images) == 0:
|
|
30
|
+
raise ValueError("No images found in dataset.")
|
|
31
|
+
if len(images) > 1:
|
|
32
|
+
raise ValueError("Multi-image files not currently supported.")
|
|
33
|
+
image = t.cast(h5py.Group, next(iter(images.values())))
|
|
34
|
+
raw_meta: numpy.ndarray = t.cast(h5py.Dataset, image['Metadata'])[:, 0][()]
|
|
35
|
+
meta_bytes = raw_meta.tobytes()
|
|
36
|
+
meta_bytes = meta_bytes[:meta_bytes.find(b"\0")]
|
|
37
|
+
meta = json.loads(meta_bytes)
|
|
38
|
+
data = t.cast(h5py.Dataset, image['Data'])[..., 0][()]
|
|
39
|
+
|
|
40
|
+
return (data, meta)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def normed_to_uint8(data: numpy.ndarray) -> NDArray[numpy.uint8]:
|
|
44
|
+
return numpy.floor(numpy.clip(data, 0, 1) * 255.999).astype(numpy.uint8)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def normed_to_color(data: numpy.ndarray, color: ArrayLike) -> NDArray[numpy.uint8]:
|
|
48
|
+
return numpy.floor(numpy.clip(data, 0, 1)[..., None] * color).astype(numpy.uint8)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def signed_angle(v1: numpy.ndarray, v2: numpy.ndarray) -> float:
|
|
52
|
+
return numpy.pi - numpy.mod(numpy.arctan2(v1[0] * v2[1] - v1[1] * v2[0], numpy.dot(v1, v2)), 2.*numpy.pi)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def load_files(paths: t.Iterable[t.Union[str, Path]], inner: float, outer: float) -> t.Iterable[t.Tuple[Path, EmpadMetadata, NDArray[numpy.float32]]]:
|
|
56
|
+
queue: Queue[t.Tuple[Path, EmpadMetadata, NDArray[numpy.float32]]] = Queue(1)
|
|
57
|
+
finished = Event()
|
|
58
|
+
|
|
59
|
+
# producer thread
|
|
60
|
+
# should hold one in queue, one waiting for queue, one processing
|
|
61
|
+
def producer():
|
|
62
|
+
try:
|
|
63
|
+
for path in paths:
|
|
64
|
+
# eagerly load and put on queue
|
|
65
|
+
path = Path(path).resolve()
|
|
66
|
+
meta = EmpadMetadata.from_json(path)
|
|
67
|
+
|
|
68
|
+
exp_path = meta.path or Path('.')
|
|
69
|
+
raw_path = (exp_path / (meta.raw_filename or "scan_x128_y128.raw")).resolve()
|
|
70
|
+
if not raw_path.exists():
|
|
71
|
+
raise ValueError(f"Can't find raw data at path '{raw_path}'")
|
|
72
|
+
scan_shape = t.cast(t.Tuple[int, int], meta.scan_shape[::-1]) if meta.scan_shape is not None else None
|
|
73
|
+
raw = load_4d(raw_path, scan_shape=scan_shape, flips=meta.det_flips)
|
|
74
|
+
|
|
75
|
+
kx = numpy.arange(raw.shape[-2], dtype=numpy.float32) - raw.shape[-2] / 2.
|
|
76
|
+
ky = numpy.arange(raw.shape[-1], dtype=numpy.float32) - raw.shape[-1] / 2.
|
|
77
|
+
kyy, kxx = numpy.meshgrid(kx, ky, indexing='ij')
|
|
78
|
+
k2 = kyy**2 + kxx**2
|
|
79
|
+
virtual_aperture = numpy.zeros(raw.shape[-2:], dtype=bool)
|
|
80
|
+
virtual_aperture[(k2 >= inner**2) & (k2 <= outer**2)] = 1
|
|
81
|
+
virtual_img = numpy.sum(raw * virtual_aperture, axis=(-1, -2))
|
|
82
|
+
|
|
83
|
+
queue.put((path, meta, virtual_img))
|
|
84
|
+
# finished all files
|
|
85
|
+
finished.set()
|
|
86
|
+
except BaseException:
|
|
87
|
+
import traceback
|
|
88
|
+
traceback.print_exc()
|
|
89
|
+
|
|
90
|
+
thread = Thread(target=producer, name='loader', daemon=True)
|
|
91
|
+
thread.start()
|
|
92
|
+
|
|
93
|
+
# periodically check that thread is still running,
|
|
94
|
+
# to prevent deadlock
|
|
95
|
+
while thread.is_alive():
|
|
96
|
+
try:
|
|
97
|
+
val = queue.get(timeout=1.)
|
|
98
|
+
except Empty:
|
|
99
|
+
continue
|
|
100
|
+
yield val
|
|
101
|
+
|
|
102
|
+
thread.join()
|
|
103
|
+
|
|
104
|
+
# drain queue once thread is finished
|
|
105
|
+
while True:
|
|
106
|
+
try:
|
|
107
|
+
val = queue.get(False)
|
|
108
|
+
except Empty:
|
|
109
|
+
break
|
|
110
|
+
yield val
|
|
111
|
+
|
|
112
|
+
if not finished.is_set():
|
|
113
|
+
raise ValueError("Error in file loading")
|
|
114
|
+
|
|
115
|
+
print("Finished processing files!")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def calibrated_meta_path(meta_path: Path):
|
|
119
|
+
meta_path_name = meta_path.stem
|
|
120
|
+
if meta_path_name.endswith('_orig'):
|
|
121
|
+
meta_path_name = meta_path_name[:-5]
|
|
122
|
+
new_meta_path = meta_path.with_stem(meta_path_name + "_calib")
|
|
123
|
+
return new_meta_path
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@click.command()
|
|
127
|
+
@click.argument('path', type=click.Path(exists=True, dir_okay=True, file_okay=True))
|
|
128
|
+
@click.option('--include', type=str, multiple=True,
|
|
129
|
+
help="Glob of filenames to include. If not specified, include all '.json' files")
|
|
130
|
+
@click.option('--exclude', type=str, multiple=True,
|
|
131
|
+
help="Glob of filenames to exclude.")
|
|
132
|
+
@click.option('--skip-existing/--no-skip-existing', default=True,
|
|
133
|
+
help="Whether to skip datasets which have already been calibrated. Defaults to true.")
|
|
134
|
+
def calc_drift(path: t.Union[str, Path], include: t.Sequence[str] = (), exclude: t.Sequence[str] = (), skip_existing: bool = True):
|
|
135
|
+
"""
|
|
136
|
+
Calculate the linear drift present in a ptychography dataset, or group of datasets.
|
|
137
|
+
|
|
138
|
+
PATH should be the path to a JSON metadata file, or to a directory
|
|
139
|
+
which will be searched for JSON metadata files.
|
|
140
|
+
|
|
141
|
+
Datasets can be included or excluded with the `--include` and `--exclude` options.
|
|
142
|
+
These can be repeated multiple times.
|
|
143
|
+
|
|
144
|
+
Calculated drift is stored in a new metadata file with the suffix `_calib`.
|
|
145
|
+
By default, datasets which already have this file are skipped. This behavior can be changed
|
|
146
|
+
using the `--no-skip-existing` option.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
console = Console()
|
|
150
|
+
path = Path(path)
|
|
151
|
+
|
|
152
|
+
if path.is_file():
|
|
153
|
+
paths = [path]
|
|
154
|
+
else:
|
|
155
|
+
exclude = (*exclude, "*_calib.json", '._*')
|
|
156
|
+
paths = list(path.glob('**/*.json'))
|
|
157
|
+
if len(include):
|
|
158
|
+
paths = list(filter(lambda path: any(fnmatch.fnmatch(path.name, pat) for pat in include), paths))
|
|
159
|
+
paths = list(filter(lambda path: not any(fnmatch.fnmatch(path.name, pat) for pat in exclude), paths))
|
|
160
|
+
|
|
161
|
+
if skip_existing:
|
|
162
|
+
paths = list(filter(lambda path: not calibrated_meta_path(path).exists(), paths))
|
|
163
|
+
|
|
164
|
+
paths.sort()
|
|
165
|
+
|
|
166
|
+
console.print(f"{len(paths)} file(s) to process.")
|
|
167
|
+
|
|
168
|
+
params = {
|
|
169
|
+
'det': 'bf',
|
|
170
|
+
'inner': 0.0,
|
|
171
|
+
'outer': 2.0,
|
|
172
|
+
'd1': 6.,
|
|
173
|
+
'd2': 6.
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
params['det'] = Prompt.ask("Detector type", choices=['bf', 'adf', 'af'], default=params['det'], console=console)
|
|
177
|
+
if params['det'] in ('af', 'adf'):
|
|
178
|
+
params['inner'] = float(FloatPrompt.ask(r"Inner radius \[mrad]", default=params['inner'], console=console))
|
|
179
|
+
inner = params['inner']
|
|
180
|
+
else:
|
|
181
|
+
inner = 0.
|
|
182
|
+
|
|
183
|
+
if params['det'] in ('af', 'bf'):
|
|
184
|
+
params['outer'] = float(FloatPrompt.ask(r"Outer radius \[mrad]", default=params['outer'], console=console))
|
|
185
|
+
outer = params['outer']
|
|
186
|
+
else:
|
|
187
|
+
outer = numpy.inf
|
|
188
|
+
|
|
189
|
+
params['scale1'] = FloatPrompt.ask("Distance 1 scale", default=1., console=console)
|
|
190
|
+
params['scale2'] = FloatPrompt.ask("Distance 2 scale", default=params['scale1'], console=console)
|
|
191
|
+
#params['angle'] = FloatPrompt.ask("Signed angle from distance 1 -> distance 2 (degree, CWW is +)", default=90., console=console)
|
|
192
|
+
#params['angle'] *= numpy.pi/180.
|
|
193
|
+
|
|
194
|
+
console.print("Loading files...")
|
|
195
|
+
for (meta_path, meta, virtual_img) in load_files(paths, inner, outer):
|
|
196
|
+
console.print(f"Loaded file '{meta.path}'")
|
|
197
|
+
if not bool(Confirm.ask("Process this file?", default=True)):
|
|
198
|
+
console.print("Skipping file...")
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
while True:
|
|
202
|
+
correction_matrix = calc_drift_one(console, meta, virtual_img, params)
|
|
203
|
+
if correction_matrix is None:
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
choice = Prompt.ask("Save calibration?", choices=['y', 'n', 'abort'], default='y', console=console)
|
|
207
|
+
if choice == 'abort':
|
|
208
|
+
break
|
|
209
|
+
if choice.lower() not in ('y', 'yes'):
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
new_meta_path = calibrated_meta_path(meta_path)
|
|
213
|
+
new_meta = meta.__replace__()
|
|
214
|
+
new_meta.scan_correction = tuple(map(tuple, correction_matrix)) # type: ignore
|
|
215
|
+
|
|
216
|
+
new_meta.write_json(new_meta_path, indent=4)
|
|
217
|
+
console.print(f"New metadata written to '{new_meta_path}'!")
|
|
218
|
+
break
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def calc_drift_one(
|
|
222
|
+
console: Console, meta: EmpadMetadata, virtual_img: NDArray[numpy.float32], params: t.Dict[str, t.Any]
|
|
223
|
+
) -> t.Optional[NDArray[numpy.number]]:
|
|
224
|
+
print(meta)
|
|
225
|
+
scan_step_4d = numpy.array(meta.scan_step) * 1e10 # m to angstrom
|
|
226
|
+
scan_size_4d = scan_step_4d * meta.scan_shape
|
|
227
|
+
|
|
228
|
+
console.print(f" 4D pixel size: {scan_step_4d[0]:.3f} x {scan_step_4d[1]:.3f} A", style='logging.level.info')
|
|
229
|
+
console.print(f" 4D image size: {scan_size_4d[0]:.2f} x {scan_size_4d[1]:.2f} A", style='logging.level.info')
|
|
230
|
+
|
|
231
|
+
fig, ax = pyplot.subplots(constrained_layout=True)
|
|
232
|
+
canvas = fig.canvas
|
|
233
|
+
ax.set_xlabel('x [A]')
|
|
234
|
+
ax.set_ylabel('y [A]')
|
|
235
|
+
|
|
236
|
+
ax.set_xlim(0., scan_size_4d[0])
|
|
237
|
+
ax.set_ylim(scan_size_4d[1], 0.)
|
|
238
|
+
|
|
239
|
+
img = ax.imshow(virtual_img, extent=(-0.5 * scan_step_4d[0], (virtual_img.shape[1] + 0.5) * scan_step_4d[0], (virtual_img.shape[0] + 0.5) * scan_step_4d[1], -0.5 * scan_step_4d[1]))
|
|
240
|
+
img.set_picker(True)
|
|
241
|
+
img.set_animated(True)
|
|
242
|
+
|
|
243
|
+
path: t.Optional[PathPatch] = None
|
|
244
|
+
circles: t.List[Circle] = []
|
|
245
|
+
|
|
246
|
+
selected: t.Optional[Circle] = None
|
|
247
|
+
bg = canvas.copy_from_bbox(ax.bbox) # type: ignore
|
|
248
|
+
|
|
249
|
+
def draw_artists():
|
|
250
|
+
ax.draw_artist(img)
|
|
251
|
+
if path is not None:
|
|
252
|
+
ax.draw_artist(path)
|
|
253
|
+
for circle in circles:
|
|
254
|
+
ax.draw_artist(circle)
|
|
255
|
+
canvas.blit(ax.bbox) # type: ignore
|
|
256
|
+
|
|
257
|
+
def draw(event=None):
|
|
258
|
+
nonlocal path, bg
|
|
259
|
+
|
|
260
|
+
vertices = numpy.array([circle.center for circle in circles])
|
|
261
|
+
if len(vertices):
|
|
262
|
+
if path is None:
|
|
263
|
+
path = PathPatch(MplPath(vertices), fill=False, lw=3.)
|
|
264
|
+
ax.add_patch(path)
|
|
265
|
+
else:
|
|
266
|
+
path.set_path(MplPath(vertices))
|
|
267
|
+
|
|
268
|
+
bg = canvas.copy_from_bbox(ax.bbox) # type: ignore
|
|
269
|
+
draw_artists()
|
|
270
|
+
|
|
271
|
+
def on_release(event: MouseEvent):
|
|
272
|
+
nonlocal selected
|
|
273
|
+
if not event.button == MouseButton.LEFT:
|
|
274
|
+
return
|
|
275
|
+
selected = None
|
|
276
|
+
|
|
277
|
+
def on_pick(event: PickEvent):
|
|
278
|
+
nonlocal selected
|
|
279
|
+
nonlocal path
|
|
280
|
+
if event.mouseevent.button != MouseButton.LEFT:
|
|
281
|
+
return
|
|
282
|
+
if event.artist is not img:
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
for circle in circles:
|
|
286
|
+
if circle.contains_point((event.mouseevent.x, event.mouseevent.y)): # type: ignore
|
|
287
|
+
selected = circle
|
|
288
|
+
return
|
|
289
|
+
|
|
290
|
+
if len(circles) < 3:
|
|
291
|
+
pos = t.cast(
|
|
292
|
+
t.Tuple[float, float],
|
|
293
|
+
list(ax.transData.inverted().transform((event.mouseevent.x, event.mouseevent.y)))
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
node = Circle(pos, radius=0.5, fc='white', ec='black', transform=ax.transData)
|
|
297
|
+
node.set_animated(True)
|
|
298
|
+
ax.add_patch(node)
|
|
299
|
+
circles.append(node)
|
|
300
|
+
selected = node
|
|
301
|
+
draw()
|
|
302
|
+
|
|
303
|
+
def on_move(event: MouseEvent):
|
|
304
|
+
if selected is None or event.inaxes is None or event.button != MouseButton.LEFT:
|
|
305
|
+
return
|
|
306
|
+
if event.x is None or event.y is None:
|
|
307
|
+
return
|
|
308
|
+
new_pt = ax.transData.inverted().transform((event.x, event.y))
|
|
309
|
+
selected.center = new_pt
|
|
310
|
+
|
|
311
|
+
canvas.restore_region(bg) # type: ignore
|
|
312
|
+
draw()
|
|
313
|
+
#draw_artists()
|
|
314
|
+
|
|
315
|
+
warped_img = None
|
|
316
|
+
correction_matrix: t.Optional[NDArray[numpy.number]] = None
|
|
317
|
+
|
|
318
|
+
def on_press(event: KeyEvent):
|
|
319
|
+
if event.key != 'enter':
|
|
320
|
+
return
|
|
321
|
+
vertices = numpy.array([circle.center for circle in circles])
|
|
322
|
+
|
|
323
|
+
if len(vertices) != 3:
|
|
324
|
+
console.print("Need 3 points to compute drift")
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
vecs = numpy.diff(vertices, axis=0)
|
|
328
|
+
#vecs_next = numpy.roll(vecs, 1, axis=0)
|
|
329
|
+
|
|
330
|
+
print(f"angle: {signed_angle(vecs[:, 0], vecs[:, 1]) * 180./numpy.pi}")
|
|
331
|
+
|
|
332
|
+
while True:
|
|
333
|
+
dists = []
|
|
334
|
+
for (i, scale, default) in zip(range(len(vertices)-1), (params['scale1'], params['scale2']), (params['d1'], params['d2'])):
|
|
335
|
+
while True:
|
|
336
|
+
try:
|
|
337
|
+
s = Prompt.ask(f"Distance {i+1} (* {scale:.3f} A)", default=default, console=console)
|
|
338
|
+
val = float(eval(s))
|
|
339
|
+
dists.append(val * scale)
|
|
340
|
+
except Exception:
|
|
341
|
+
continue
|
|
342
|
+
break
|
|
343
|
+
console.print(f"Distances [A]: {dists[0]:.3f}, {dists[1]:.3f}")
|
|
344
|
+
if Confirm.ask("Distances correct?", default=True):
|
|
345
|
+
break
|
|
346
|
+
|
|
347
|
+
# first, we use some logic to determine which orthogonal basis the measurements are closest to.
|
|
348
|
+
horz_vec = numpy.argmax(numpy.abs(vecs[:, 0])) # vec with maximum x component is horizontal
|
|
349
|
+
horz_sign = numpy.sign(vecs[horz_vec, 0]) # whether to flip horizontal vector
|
|
350
|
+
vert_sign = numpy.sign(vecs[horz_vec-1, 1]) # whether to flip vertical vector
|
|
351
|
+
|
|
352
|
+
# make target_vecs using the determinations above
|
|
353
|
+
target_vecs = numpy.diag(numpy.array(dists))
|
|
354
|
+
if horz_vec != 0:
|
|
355
|
+
# v1 should be vertical
|
|
356
|
+
target_vecs = target_vecs[::-1, :]
|
|
357
|
+
# flip target_vecs based on desired signs
|
|
358
|
+
target_vecs = numpy.diag([horz_sign, vert_sign]) @ target_vecs
|
|
359
|
+
console.print(f"finding transformation\n{vecs.T}\nto\n{target_vecs}")
|
|
360
|
+
|
|
361
|
+
# we try to find A which transforms `vecs` into `target_vecs`.
|
|
362
|
+
a = target_vecs @ numpy.linalg.inv(vecs.T)
|
|
363
|
+
|
|
364
|
+
nonlocal warped_img, correction_matrix
|
|
365
|
+
|
|
366
|
+
# because we only know distances, `a`
|
|
367
|
+
q, r = numpy.linalg.qr(a)
|
|
368
|
+
r: NDArray[numpy.floating] = numpy.diag(numpy.sign(numpy.diagonal(r))) @ r # flip to ensure diagonals are positive. These are absorbed into `q`.
|
|
369
|
+
console.print(f"actual dists: {dists}")
|
|
370
|
+
console.print(f"measured dists: {numpy.linalg.norm(vecs, axis=-1)}")
|
|
371
|
+
console.print(f"distortion:\n{r[::-1, ::-1]}") # correct for ptychoshelves coordinate system
|
|
372
|
+
#print(f"distortion:\n{r}") # correct for ptychoshelves coordinate system
|
|
373
|
+
vecs_after = (r @ vecs.T).T
|
|
374
|
+
console.print(f"dists after correction: {numpy.linalg.norm(vecs_after, axis=-1)}")
|
|
375
|
+
console.print(f"vecs after correction: {vecs_after[0]}, {vecs_after[1]}")
|
|
376
|
+
angle_after = signed_angle(vecs_after[0], vecs_after[1])
|
|
377
|
+
console.print(f"angle after correction: {180./numpy.pi * angle_after:.2f}")
|
|
378
|
+
#vecs_after_a = (a @ vecs.T).T
|
|
379
|
+
#print(f"vecs after a correction: {vecs_after_a[:, 0]}, {vecs_after_a[:, 1]}")
|
|
380
|
+
#angle_after_a = signed_angle(vecs_after_a[:, 0], vecs_after_a[:, 1])
|
|
381
|
+
#print(f"angle after a correction: {180./numpy.pi * angle_after_a:.2f}")
|
|
382
|
+
correction_matrix = r
|
|
383
|
+
|
|
384
|
+
# warp image given corrections
|
|
385
|
+
warped_shape = tuple(numpy.ceil(numpy.array(virtual_img.shape) * numpy.max(numpy.abs(numpy.diagonal(r)))).astype(int))
|
|
386
|
+
affine = numpy.block([[numpy.linalg.inv(r)[::-1, ::-1], numpy.zeros((2, 1))], [numpy.zeros((1, 2)), numpy.ones((1, 1))]])
|
|
387
|
+
translation = numpy.eye(3)
|
|
388
|
+
translation[:2, -1] += numpy.array(virtual_img.shape[-2:]) / 2.
|
|
389
|
+
translation2 = numpy.eye(3)
|
|
390
|
+
translation2[:2, -1] -= numpy.array(warped_shape[-2:]) / 2.
|
|
391
|
+
affine = translation @ affine @ translation2
|
|
392
|
+
warped_img = scipy.ndimage.affine_transform(virtual_img, affine, output_shape=warped_shape)
|
|
393
|
+
pyplot.close(fig)
|
|
394
|
+
|
|
395
|
+
# TODO: error out for non-interactive backends
|
|
396
|
+
#canvas.mpl_connect('button_press_event', on_click)
|
|
397
|
+
canvas.mpl_connect('button_release_event', on_release) # type: ignore
|
|
398
|
+
canvas.mpl_connect('key_press_event', on_press) # type: ignore
|
|
399
|
+
canvas.mpl_connect('motion_notify_event', on_move) # type: ignore
|
|
400
|
+
canvas.mpl_connect('draw_event', draw) # type: ignore
|
|
401
|
+
canvas.mpl_connect('pick_event', on_pick) # type: ignore
|
|
402
|
+
|
|
403
|
+
pyplot.show()
|
|
404
|
+
|
|
405
|
+
if warped_img is None:
|
|
406
|
+
return
|
|
407
|
+
|
|
408
|
+
fig, ax = pyplot.subplots()
|
|
409
|
+
ax.imshow(warped_img, vmin=float(numpy.nanmin(virtual_img)), vmax=float(numpy.nanmax(virtual_img)),
|
|
410
|
+
extent=(-0.5 * scan_step_4d[0], (warped_img.shape[1] + 0.5) * scan_step_4d[0], (warped_img.shape[0] + 0.5) * scan_step_4d[1], -0.5 * scan_step_4d[1]))
|
|
411
|
+
ax.set_xlabel('x [A]')
|
|
412
|
+
ax.set_ylabel('y [A]')
|
|
413
|
+
ax.set_title("Warped image")
|
|
414
|
+
|
|
415
|
+
def on_press_2(event: KeyEvent):
|
|
416
|
+
if event.key != 'enter':
|
|
417
|
+
return
|
|
418
|
+
pyplot.close(fig)
|
|
419
|
+
|
|
420
|
+
fig.canvas.mpl_connect('key_press_event', on_press_2) # type: ignore
|
|
421
|
+
|
|
422
|
+
pyplot.show()
|
|
423
|
+
|
|
424
|
+
return correction_matrix
|