atomscale 0.10.2__tar.gz → 0.12.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.
- {atomscale-0.10.2 → atomscale-0.12.0}/PKG-INFO +1 -1
- atomscale-0.12.0/examples/changepoints.ipynb +191 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/pyproject.toml +1 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/client.py +211 -32
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/results/__init__.py +4 -0
- atomscale-0.12.0/src/atomscale/results/changepoint.py +67 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/results/metrology.py +4 -1
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/results/optical.py +3 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/results/photoluminescence.py +3 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/results/raman.py +3 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/results/rheed_image.py +4 -1
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/results/rheed_video.py +3 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/results/unknown.py +2 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/results/xps.py +3 -0
- atomscale-0.12.0/src/atomscale/results/xrd.py +61 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/streaming/rheed_stream.pyi +10 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/streaming/src/initialize.rs +126 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/streaming/src/lib.rs +23 -4
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/streaming/src/timeseries.rs +25 -4
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale.egg-info/PKG-INFO +1 -1
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale.egg-info/SOURCES.txt +4 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/tests/_mock_http_server.py +17 -6
- {atomscale-0.10.2 → atomscale-0.12.0}/tests/conftest.py +1 -0
- atomscale-0.12.0/tests/test_changepoint.py +51 -0
- atomscale-0.12.0/tests/test_rheed_stream.py +545 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/tests/test_timeseries_stream.py +43 -0
- atomscale-0.10.2/tests/test_rheed_stream.py +0 -279
- {atomscale-0.10.2 → atomscale-0.12.0}/.github/workflows/release.yml +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/.github/workflows/testing.yml +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/.github/workflows/upgrade_dependencies.yml +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/.gitignore +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/.pre-commit-config.yaml +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/CHANGELOG.md +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/LICENSE +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/MANIFEST.in +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/README.md +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/atomicds-shim-dist/pyproject.toml +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/docs/Makefile +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/docs/_static/AtomscaleLogoFull.png +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/docs/_static/custom.css +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/docs/_templates/custom-class-template.rst +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/docs/_templates/custom-module-template.rst +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/docs/conf.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/docs/guides/analysis-results.rst +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/docs/guides/monitor-live.rst +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/docs/guides/quickstart.rst +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/docs/guides/search-and-download.rst +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/docs/guides/stream-rheed.rst +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/docs/guides/stream-timeseries.rst +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/docs/guides/upload-files.rst +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/docs/index.rst +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/docs/make.bat +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/docs/modules.rst +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/examples/general_use.ipynb +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/examples/rheed_streaming.ipynb +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/examples/timeseries_polling.ipynb +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/examples/vxwse2-placeholder/task1_films.ipynb +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/examples/vxwse2-placeholder/task1_sapphire.ipynb +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/examples/vxwse2-placeholder/task2_composition.ipynb +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/requirements/requirements-macos-latest_py3.10.txt +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/requirements/requirements-macos-latest_py3.10_extras.txt +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/requirements/requirements-macos-latest_py3.11.txt +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/requirements/requirements-macos-latest_py3.11_extras.txt +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/requirements/requirements-macos-latest_py3.12.txt +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/requirements/requirements-macos-latest_py3.12_extras.txt +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/requirements/requirements-macos-latest_py3.9.txt +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/requirements/requirements-macos-latest_py3.9_extras.txt +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/requirements/requirements-ubuntu-latest_py3.10.txt +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/requirements/requirements-ubuntu-latest_py3.10_extras.txt +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/requirements/requirements-ubuntu-latest_py3.11.txt +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/requirements/requirements-ubuntu-latest_py3.11_extras.txt +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/requirements/requirements-ubuntu-latest_py3.12.txt +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/requirements/requirements-ubuntu-latest_py3.12_extras.txt +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/requirements/requirements-ubuntu-latest_py3.9.txt +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/requirements/requirements-ubuntu-latest_py3.9_extras.txt +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/requirements/requirements-windows-latest_py3.10.txt +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/requirements/requirements-windows-latest_py3.10_extras.txt +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/requirements/requirements-windows-latest_py3.11.txt +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/requirements/requirements-windows-latest_py3.11_extras.txt +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/requirements/requirements-windows-latest_py3.12.txt +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/requirements/requirements-windows-latest_py3.12_extras.txt +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/requirements/requirements-windows-latest_py3.9.txt +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/requirements/requirements-windows-latest_py3.9_extras.txt +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/setup.cfg +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomicds/__init__.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/__init__.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/core/__init__.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/core/client.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/core/files.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/core/utils.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/results/group.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/results/similarity_trajectory.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/similarity/__init__.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/similarity/polling.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/similarity/provider.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/streaming/Cargo.lock +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/streaming/Cargo.toml +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/streaming/__init__.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/streaming/rheed_stream.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/streaming/src/upload.rs +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/streaming/src/utils.rs +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/timeseries/__init__.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/timeseries/_polling_utils.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/timeseries/align.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/timeseries/metrology.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/timeseries/optical.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/timeseries/polling.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/timeseries/provider.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/timeseries/registry.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/timeseries/rheed.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale/timeseries/sample.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale.egg-info/dependency_links.txt +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale.egg-info/requires.txt +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/src/atomscale.egg-info/top_level.txt +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/tests/__init__.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/tests/data/test_rheed.mp4 +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/tests/test_atomicds_alias.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/tests/test_client.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/tests/test_core.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/tests/test_metrology.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/tests/test_optical.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/tests/test_photoluminescence.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/tests/test_polling.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/tests/test_raman.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/tests/test_rheed_image.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/tests/test_rheed_video.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/tests/test_similarity_trajectory.py +0 -0
- {atomscale-0.10.2 → atomscale-0.12.0}/tests/test_xps.py +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cells": [
|
|
3
|
+
{
|
|
4
|
+
"cell_type": "markdown",
|
|
5
|
+
"metadata": {},
|
|
6
|
+
"source": [
|
|
7
|
+
"# Changepoint Detection\n",
|
|
8
|
+
"This short notebook shows how to pull changepoint detection records from the Atomscale API using `client.get_changepoints()`. Changepoints are time-bounded regions where an automated detector flagged a meaningful shift in one of the analyzed properties of a data item."
|
|
9
|
+
]
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"cell_type": "markdown",
|
|
13
|
+
"metadata": {},
|
|
14
|
+
"source": [
|
|
15
|
+
"### Install package"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"cell_type": "code",
|
|
20
|
+
"execution_count": null,
|
|
21
|
+
"metadata": {},
|
|
22
|
+
"outputs": [],
|
|
23
|
+
"source": [
|
|
24
|
+
"#!pip install atomscale"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"cell_type": "markdown",
|
|
29
|
+
"metadata": {},
|
|
30
|
+
"source": [
|
|
31
|
+
"### API Client Setup"
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"cell_type": "code",
|
|
36
|
+
"execution_count": null,
|
|
37
|
+
"metadata": {},
|
|
38
|
+
"outputs": [],
|
|
39
|
+
"source": [
|
|
40
|
+
"from atomscale import Client\n",
|
|
41
|
+
"from atomscale.results import ChangepointResult\n",
|
|
42
|
+
"\n",
|
|
43
|
+
"api_key = \"YOUR_API_KEY_HERE\"\n",
|
|
44
|
+
"client = Client(api_key=api_key)\n",
|
|
45
|
+
"\n",
|
|
46
|
+
"# Use a data ID from your catalogue. Search or grab one from the web interface.\n",
|
|
47
|
+
"data_id = \"YOUR_DATA_ID_HERE\""
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"cell_type": "markdown",
|
|
52
|
+
"metadata": {},
|
|
53
|
+
"source": [
|
|
54
|
+
"### Fetching Changepoints\n",
|
|
55
|
+
"Call `get_changepoints` with one or more `data_ids` to get back a pandas `DataFrame` of detected changepoints. With default arguments it returns the most recent intensity-profile detection run, filtered to the critical severity tier — a good starting point for most users."
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"cell_type": "code",
|
|
60
|
+
"execution_count": null,
|
|
61
|
+
"metadata": {},
|
|
62
|
+
"outputs": [],
|
|
63
|
+
"source": [
|
|
64
|
+
"changepoints = client.get_changepoints(data_ids=data_id)\n",
|
|
65
|
+
"changepoints"
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"cell_type": "markdown",
|
|
70
|
+
"metadata": {},
|
|
71
|
+
"source": [
|
|
72
|
+
"Each row is a single changepoint. The columns are:\n",
|
|
73
|
+
"```\n",
|
|
74
|
+
"['id', 'data_id', 'data_modality', 'property_name', 'severity', 'score',\n",
|
|
75
|
+
" 'window_start_elapsed', 'window_end_elapsed', 'detection_method', 'detail', 'label']\n",
|
|
76
|
+
"```\n",
|
|
77
|
+
"- `score` is a normalized magnitude in `[0, 1]` — higher means stronger signal.\n",
|
|
78
|
+
"- `window_start_elapsed` / `window_end_elapsed` are seconds from the start of the source time series, so you can line changepoints up against timeseries data pulled via `client.get()`.\n",
|
|
79
|
+
"- `detail` holds method-specific metadata (e.g. RMS error, comparison window size).\n",
|
|
80
|
+
"- `label` is the applied category label if one has been set, otherwise `None`."
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"cell_type": "markdown",
|
|
85
|
+
"metadata": {},
|
|
86
|
+
"source": [
|
|
87
|
+
"### Filtering Options\n",
|
|
88
|
+
"The defaults keep the view focused, but all three filter arguments can be adjusted:\n",
|
|
89
|
+
"\n",
|
|
90
|
+
"- `detection_method` — `\"forecasting\"`, `\"clustering\"`, `\"intensity_profile\"`, or `None` for all methods. Defaults to `\"intensity_profile\"`.\n",
|
|
91
|
+
"- `severity` — `\"info\"`, `\"warning\"`, `\"critical\"`, or `None` for all levels. Defaults to `\"critical\"`.\n",
|
|
92
|
+
"- `latest_only` — when `True` (default), only anomalies from the most recently completed run per `(data_id, detection_method)` are returned. Set to `False` to include every historical run."
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"cell_type": "code",
|
|
97
|
+
"execution_count": null,
|
|
98
|
+
"metadata": {},
|
|
99
|
+
"outputs": [],
|
|
100
|
+
"source": [
|
|
101
|
+
"# Everything the detector flagged, no filters applied.\n",
|
|
102
|
+
"all_changepoints = client.get_changepoints(\n",
|
|
103
|
+
" data_ids=data_id,\n",
|
|
104
|
+
" detection_method=None,\n",
|
|
105
|
+
" severity=None,\n",
|
|
106
|
+
")\n",
|
|
107
|
+
"all_changepoints[[\"detection_method\", \"severity\", \"score\", \"property_name\"]]"
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"cell_type": "code",
|
|
112
|
+
"execution_count": null,
|
|
113
|
+
"metadata": {},
|
|
114
|
+
"outputs": [],
|
|
115
|
+
"source": [
|
|
116
|
+
"# All historical runs for this data_id, not just the latest.\n",
|
|
117
|
+
"history = client.get_changepoints(\n",
|
|
118
|
+
" data_ids=data_id,\n",
|
|
119
|
+
" detection_method=None,\n",
|
|
120
|
+
" severity=None,\n",
|
|
121
|
+
" latest_only=False,\n",
|
|
122
|
+
")\n",
|
|
123
|
+
"print(f\"latest run only: {len(all_changepoints)} rows\")\n",
|
|
124
|
+
"print(f\"all historical runs: {len(history)} rows\")"
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"cell_type": "markdown",
|
|
129
|
+
"metadata": {},
|
|
130
|
+
"source": [
|
|
131
|
+
"### Result Objects\n",
|
|
132
|
+
"Pass `as_dataframe=False` to get a list of `ChangepointResult` objects instead of a DataFrame. Useful when you want to iterate and work with the records programmatically."
|
|
133
|
+
]
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"cell_type": "code",
|
|
137
|
+
"execution_count": null,
|
|
138
|
+
"metadata": {},
|
|
139
|
+
"outputs": [],
|
|
140
|
+
"source": [
|
|
141
|
+
"results = client.get_changepoints(data_ids=data_id, as_dataframe=False)\n",
|
|
142
|
+
"\n",
|
|
143
|
+
"for cp in results[:5]:\n",
|
|
144
|
+
" window = f\"[{cp.window_start_elapsed:.1f}s \\u2192 {cp.window_end_elapsed:.1f}s]\"\n",
|
|
145
|
+
" print(f\"{cp.severity:>8} score={cp.score:.3f} {window} {cp.property_name}\")"
|
|
146
|
+
]
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"cell_type": "markdown",
|
|
150
|
+
"metadata": {},
|
|
151
|
+
"source": [
|
|
152
|
+
"### Batch Over Multiple Data IDs\n",
|
|
153
|
+
"`data_ids` accepts a list; requests are chunked under the hood so you can pass an arbitrary number of IDs in a single call."
|
|
154
|
+
]
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"cell_type": "code",
|
|
158
|
+
"execution_count": null,
|
|
159
|
+
"metadata": {},
|
|
160
|
+
"outputs": [],
|
|
161
|
+
"source": [
|
|
162
|
+
"# Use a search to pull a batch of data IDs, then fetch changepoints across all of them.\n",
|
|
163
|
+
"search_results = client.search(data_type=\"rheed_stationary\", status=\"success\")\n",
|
|
164
|
+
"data_ids = search_results[\"Data ID\"].to_list()\n",
|
|
165
|
+
"\n",
|
|
166
|
+
"batch = client.get_changepoints(data_ids=data_ids)\n",
|
|
167
|
+
"batch.groupby(\"data_id\").size().sort_values(ascending=False).head()"
|
|
168
|
+
]
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"cell_type": "markdown",
|
|
172
|
+
"metadata": {},
|
|
173
|
+
"source": [
|
|
174
|
+
"For more information on other data from the API or other example use see notebooks in the code repository (https://github.com/atomic-data-sciences/api-client) and the documentation (https://atomic-data-sciences.github.io/api-client/)"
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
],
|
|
178
|
+
"metadata": {
|
|
179
|
+
"kernelspec": {
|
|
180
|
+
"display_name": "Python 3",
|
|
181
|
+
"language": "python",
|
|
182
|
+
"name": "python3"
|
|
183
|
+
},
|
|
184
|
+
"language_info": {
|
|
185
|
+
"name": "python",
|
|
186
|
+
"pygments_lexer": "ipython3"
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
"nbformat": 4,
|
|
190
|
+
"nbformat_minor": 5
|
|
191
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
+
import re
|
|
6
7
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
7
8
|
from datetime import datetime
|
|
8
9
|
from pathlib import Path
|
|
@@ -14,12 +15,14 @@ from pandas import DataFrame
|
|
|
14
15
|
from atomscale.core import BaseClient, ClientError, _FileSlice
|
|
15
16
|
from atomscale.core.utils import _make_progress, normalize_path
|
|
16
17
|
from atomscale.results import (
|
|
18
|
+
ChangepointResult,
|
|
17
19
|
PhotoluminescenceResult,
|
|
18
20
|
RamanResult,
|
|
19
21
|
RHEEDImageResult,
|
|
20
22
|
RHEEDVideoResult,
|
|
21
23
|
UnknownResult,
|
|
22
24
|
XPSResult,
|
|
25
|
+
XRDResult,
|
|
23
26
|
_get_rheed_image_result,
|
|
24
27
|
)
|
|
25
28
|
from atomscale.results.group import PhysicalSampleResult, ProjectResult
|
|
@@ -71,6 +74,7 @@ class Client(BaseClient):
|
|
|
71
74
|
"rheed_stationary",
|
|
72
75
|
"rheed_rotating",
|
|
73
76
|
"xps",
|
|
77
|
+
"xrd",
|
|
74
78
|
"photoluminescence",
|
|
75
79
|
"pl",
|
|
76
80
|
"raman",
|
|
@@ -102,7 +106,7 @@ class Client(BaseClient):
|
|
|
102
106
|
data_ids (str | list[str] | None): Data ID or list of data IDs. Defaults to None.
|
|
103
107
|
physical_sample_ids (str | list[str] | None): Physical sample ID or list of IDs. Defaults to None.
|
|
104
108
|
project_ids (str | list[str] | None): Project ID or list of IDs. Defaults to None.
|
|
105
|
-
data_type (Literal["rheed_image", "rheed_stationary", "rheed_rotating", "xps", "photoluminescence", "raman", "all"]): Type of data. Defaults to "all".
|
|
109
|
+
data_type (Literal["rheed_image", "rheed_stationary", "rheed_rotating", "xps", "xrd", "photoluminescence", "raman", "all"]): Type of data. Defaults to "all".
|
|
106
110
|
status (Literal["success", "pending", "error", "running", "all"]): Analyzed status of the data. Defaults to "all".
|
|
107
111
|
growth_length (tuple[int | None, int | None]): Minimum and maximum values of the growth length in seconds.
|
|
108
112
|
Defaults to (None, None) which will include all non-video data.
|
|
@@ -172,10 +176,10 @@ class Client(BaseClient):
|
|
|
172
176
|
|
|
173
177
|
if "projects" in catalogue.columns:
|
|
174
178
|
catalogue["project_ids"] = catalogue["projects"].apply(
|
|
175
|
-
lambda projects:
|
|
179
|
+
lambda projects: projects[0].get("id") if projects else None
|
|
176
180
|
)
|
|
177
181
|
catalogue["project_names"] = catalogue["projects"].apply(
|
|
178
|
-
lambda projects:
|
|
182
|
+
lambda projects: projects[0].get("name") if projects else None
|
|
179
183
|
)
|
|
180
184
|
|
|
181
185
|
if len(catalogue):
|
|
@@ -224,6 +228,7 @@ class Client(BaseClient):
|
|
|
224
228
|
RHEEDVideoResult
|
|
225
229
|
| RHEEDImageResult
|
|
226
230
|
| XPSResult
|
|
231
|
+
| XRDResult
|
|
227
232
|
| PhotoluminescenceResult
|
|
228
233
|
| RamanResult
|
|
229
234
|
| UnknownResult
|
|
@@ -234,7 +239,7 @@ class Client(BaseClient):
|
|
|
234
239
|
data_ids (str | list[str]): Data ID or list of data IDs from the data catalogue to obtain analyzed results for.
|
|
235
240
|
|
|
236
241
|
Returns:
|
|
237
|
-
list[atomscale.results.RHEEDVideoResult | atomscale.results.RHEEDImageResult | atomscale.results.XPSResult]:
|
|
242
|
+
list[atomscale.results.RHEEDVideoResult | atomscale.results.RHEEDImageResult | atomscale.results.XPSResult | atomscale.results.XRDResult]:
|
|
238
243
|
List of result objects
|
|
239
244
|
|
|
240
245
|
"""
|
|
@@ -285,6 +290,80 @@ class Client(BaseClient):
|
|
|
285
290
|
progress_description="Obtaining data results",
|
|
286
291
|
)
|
|
287
292
|
|
|
293
|
+
def get_changepoints(
|
|
294
|
+
self,
|
|
295
|
+
data_ids: str | list[str],
|
|
296
|
+
latest_only: bool = True,
|
|
297
|
+
detection_method: (
|
|
298
|
+
Literal["forecasting", "clustering", "intensity_profile"] | None
|
|
299
|
+
) = "intensity_profile",
|
|
300
|
+
severity: Literal["info", "warning", "critical"] | None = "critical",
|
|
301
|
+
as_dataframe: bool = True,
|
|
302
|
+
) -> DataFrame | list[ChangepointResult]:
|
|
303
|
+
"""Get changepoint detection records for one or more data IDs.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
data_ids (str | list[str]): Data ID or list of data IDs from the data catalogue.
|
|
307
|
+
latest_only (bool): If True (default), only return changepoints from the most
|
|
308
|
+
recently completed detection run for each (data_id, detection_method) pair.
|
|
309
|
+
If False, return all changepoints from every historical run.
|
|
310
|
+
detection_method (str | None): Filter to a single detection method. One of
|
|
311
|
+
"forecasting", "clustering", "intensity_profile". Defaults to "intensity_profile".
|
|
312
|
+
Pass None to include all detection methods.
|
|
313
|
+
severity (str | None): Filter to a single severity level. One of "info",
|
|
314
|
+
"warning", "critical". Defaults to "critical". Pass None to include all
|
|
315
|
+
severities.
|
|
316
|
+
as_dataframe (bool): If True (default) return a pandas DataFrame. If False
|
|
317
|
+
return a list of ChangepointResult objects.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
DataFrame | list[ChangepointResult]: Changepoint records matching the filters.
|
|
321
|
+
"""
|
|
322
|
+
if isinstance(data_ids, str):
|
|
323
|
+
data_ids = [data_ids]
|
|
324
|
+
|
|
325
|
+
records: list[dict] = []
|
|
326
|
+
chunk_size = 100
|
|
327
|
+
chunks = [
|
|
328
|
+
data_ids[i : i + chunk_size] for i in range(0, len(data_ids), chunk_size)
|
|
329
|
+
]
|
|
330
|
+
|
|
331
|
+
for chunk in chunks:
|
|
332
|
+
payload: dict | None = self._get( # type: ignore[assignment]
|
|
333
|
+
sub_url="changepoints/",
|
|
334
|
+
params={"data_ids": chunk, "latest_only": latest_only},
|
|
335
|
+
)
|
|
336
|
+
if payload:
|
|
337
|
+
records.extend(payload.get("changepoints", []))
|
|
338
|
+
|
|
339
|
+
if detection_method is not None:
|
|
340
|
+
records = [
|
|
341
|
+
r for r in records if r.get("detection_method") == detection_method
|
|
342
|
+
]
|
|
343
|
+
if severity is not None:
|
|
344
|
+
records = [r for r in records if r.get("severity") == severity]
|
|
345
|
+
|
|
346
|
+
if as_dataframe:
|
|
347
|
+
# Keep only the label itself; drop label provenance/metadata fields.
|
|
348
|
+
_drop = {
|
|
349
|
+
"label_category",
|
|
350
|
+
"label_notes",
|
|
351
|
+
"label_source",
|
|
352
|
+
"label_confidence",
|
|
353
|
+
"labeled_at",
|
|
354
|
+
"labeled_by_user_id",
|
|
355
|
+
"similar_neighbor_ids",
|
|
356
|
+
}
|
|
357
|
+
rows = [
|
|
358
|
+
{
|
|
359
|
+
**{k: v for k, v in r.items() if k not in _drop},
|
|
360
|
+
"label": r.get("label_category"),
|
|
361
|
+
}
|
|
362
|
+
for r in records
|
|
363
|
+
]
|
|
364
|
+
return DataFrame(rows)
|
|
365
|
+
return [ChangepointResult.from_api(r) for r in records]
|
|
366
|
+
|
|
288
367
|
def list_physical_samples(self) -> DataFrame:
|
|
289
368
|
"""List physical samples available to the user."""
|
|
290
369
|
data = self._get(sub_url="physical_samples/")
|
|
@@ -294,10 +373,10 @@ class Client(BaseClient):
|
|
|
294
373
|
|
|
295
374
|
if "projects" in samples.columns:
|
|
296
375
|
samples["project_id"] = samples["projects"].apply(
|
|
297
|
-
lambda projects:
|
|
376
|
+
lambda projects: projects[0].get("id") if projects else None
|
|
298
377
|
)
|
|
299
378
|
samples["project_name"] = samples["projects"].apply(
|
|
300
|
-
lambda projects:
|
|
379
|
+
lambda projects: projects[0].get("name") if projects else None
|
|
301
380
|
)
|
|
302
381
|
|
|
303
382
|
if "detail_notes" in samples.columns:
|
|
@@ -305,9 +384,9 @@ class Client(BaseClient):
|
|
|
305
384
|
lambda note: note.get("content") if isinstance(note, dict) else None
|
|
306
385
|
)
|
|
307
386
|
samples["detail_note_last_updated"] = samples["detail_notes"].apply(
|
|
308
|
-
lambda note:
|
|
309
|
-
|
|
310
|
-
|
|
387
|
+
lambda note: (
|
|
388
|
+
note.get("last_updated") if isinstance(note, dict) else None
|
|
389
|
+
)
|
|
311
390
|
)
|
|
312
391
|
samples["detail_note_last_updated"] = samples[
|
|
313
392
|
"detail_note_last_updated"
|
|
@@ -315,13 +394,15 @@ class Client(BaseClient):
|
|
|
315
394
|
|
|
316
395
|
if "target_material" in samples.columns:
|
|
317
396
|
samples["target_material"] = samples["target_material"].apply(
|
|
318
|
-
lambda tm:
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
397
|
+
lambda tm: (
|
|
398
|
+
{
|
|
399
|
+
k: tm.get(k)
|
|
400
|
+
for k in ("substrate", "sample_name")
|
|
401
|
+
if isinstance(tm, dict) and k in tm
|
|
402
|
+
}
|
|
403
|
+
if isinstance(tm, dict)
|
|
404
|
+
else tm
|
|
405
|
+
)
|
|
325
406
|
)
|
|
326
407
|
|
|
327
408
|
columns_to_drop = [
|
|
@@ -390,9 +471,9 @@ class Client(BaseClient):
|
|
|
390
471
|
lambda note: note.get("content") if isinstance(note, dict) else None
|
|
391
472
|
)
|
|
392
473
|
projects["detail_note_last_updated"] = projects["detail_note"].apply(
|
|
393
|
-
lambda note:
|
|
394
|
-
|
|
395
|
-
|
|
474
|
+
lambda note: (
|
|
475
|
+
note.get("last_updated") if isinstance(note, dict) else None
|
|
476
|
+
)
|
|
396
477
|
)
|
|
397
478
|
projects["detail_note_last_updated"] = projects[
|
|
398
479
|
"detail_note_last_updated"
|
|
@@ -552,6 +633,7 @@ class Client(BaseClient):
|
|
|
552
633
|
data_id: str,
|
|
553
634
|
data_type: Literal[
|
|
554
635
|
"xps",
|
|
636
|
+
"xrd",
|
|
555
637
|
"photoluminescence",
|
|
556
638
|
"pl",
|
|
557
639
|
"raman",
|
|
@@ -570,20 +652,41 @@ class Client(BaseClient):
|
|
|
570
652
|
| XPSResult
|
|
571
653
|
| PhotoluminescenceResult
|
|
572
654
|
| RamanResult
|
|
655
|
+
| XRDResult
|
|
573
656
|
| UnknownResult
|
|
574
657
|
| None
|
|
575
658
|
):
|
|
659
|
+
collected_dt = (
|
|
660
|
+
catalogue_entry.get("collected_datetime") if catalogue_entry else None
|
|
661
|
+
)
|
|
662
|
+
|
|
576
663
|
if data_type == "xps":
|
|
577
|
-
result: dict = self._get(sub_url=f"xps/{data_id}") # type: ignore # noqa: PGH003
|
|
664
|
+
result: dict = self._get(sub_url=f"xps/{data_id}") or {} # type: ignore # noqa: PGH003
|
|
578
665
|
|
|
579
666
|
return XPSResult(
|
|
580
667
|
data_id=data_id,
|
|
581
|
-
xps_id=result
|
|
582
|
-
binding_energies=result
|
|
583
|
-
intensities=result
|
|
584
|
-
predicted_composition=result
|
|
585
|
-
detected_peaks=result
|
|
586
|
-
elements_manually_set=bool(result
|
|
668
|
+
xps_id=result.get("xps_id"),
|
|
669
|
+
binding_energies=result.get("binding_energies", []),
|
|
670
|
+
intensities=result.get("intensities", []),
|
|
671
|
+
predicted_composition=result.get("predicted_composition", {}),
|
|
672
|
+
detected_peaks=result.get("detected_peaks", {}),
|
|
673
|
+
elements_manually_set=bool(result.get("set_elements", False)),
|
|
674
|
+
collected_datetime=collected_dt,
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
if data_type == "xrd":
|
|
678
|
+
result = self._get(sub_url=f"xrd/{data_id}") or {} # type: ignore # noqa: PGH003
|
|
679
|
+
return XRDResult(
|
|
680
|
+
data_id=data_id,
|
|
681
|
+
xrd_id=result.get("id"),
|
|
682
|
+
two_theta=result.get("two_theta", []),
|
|
683
|
+
intensities=result.get("intensities", []),
|
|
684
|
+
detected_peaks=result.get("detected_peaks", []),
|
|
685
|
+
wavelength_angstrom=result.get("wavelength_angstrom", 1.5406),
|
|
686
|
+
two_theta_unit=result.get("two_theta_unit", "degrees"),
|
|
687
|
+
spectral_metadata=result.get("spectral_metadata", {}),
|
|
688
|
+
last_updated=result.get("last_updated"),
|
|
689
|
+
collected_datetime=collected_dt,
|
|
587
690
|
)
|
|
588
691
|
|
|
589
692
|
if data_type in ("photoluminescence", "pl"):
|
|
@@ -602,6 +705,7 @@ class Client(BaseClient):
|
|
|
602
705
|
intensities=result.get("intensities", []),
|
|
603
706
|
detected_peaks=result.get("detected_peaks", {}),
|
|
604
707
|
last_updated=result.get("last_updated"),
|
|
708
|
+
collected_datetime=collected_dt,
|
|
605
709
|
)
|
|
606
710
|
|
|
607
711
|
if data_type == "raman":
|
|
@@ -613,10 +717,14 @@ class Client(BaseClient):
|
|
|
613
717
|
intensities=result.get("intensities", []),
|
|
614
718
|
detected_peaks=result.get("detected_peaks", {}),
|
|
615
719
|
last_updated=result.get("last_updated"),
|
|
720
|
+
collected_datetime=collected_dt,
|
|
616
721
|
)
|
|
617
722
|
|
|
618
723
|
if data_type == "rheed_image":
|
|
619
|
-
|
|
724
|
+
result_obj = _get_rheed_image_result(self, data_id)
|
|
725
|
+
if result_obj is not None:
|
|
726
|
+
result_obj.collected_datetime = collected_dt
|
|
727
|
+
return result_obj
|
|
620
728
|
|
|
621
729
|
if data_type in [
|
|
622
730
|
"rheed_stationary",
|
|
@@ -646,6 +754,7 @@ class Client(BaseClient):
|
|
|
646
754
|
upload_dt = catalogue_entry.get("upload_datetime")
|
|
647
755
|
if upload_dt:
|
|
648
756
|
result_obj.upload_datetime = upload_dt
|
|
757
|
+
result_obj.collected_datetime = collected_dt
|
|
649
758
|
return result_obj
|
|
650
759
|
|
|
651
760
|
# Fallback for unknown/unsupported data types
|
|
@@ -653,16 +762,80 @@ class Client(BaseClient):
|
|
|
653
762
|
data_id=data_id,
|
|
654
763
|
data_type=data_type,
|
|
655
764
|
catalogue_entry=catalogue_entry,
|
|
765
|
+
collected_datetime=collected_dt,
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
_UUID_RE = re.compile(
|
|
769
|
+
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.IGNORECASE
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
def _resolve_physical_sample(self, physical_sample: str) -> tuple[str, str]:
|
|
773
|
+
"""Resolve a physical sample name or UUID to (id, name).
|
|
774
|
+
|
|
775
|
+
If UUID: look up existing sample, error if not found.
|
|
776
|
+
If name: case-insensitive match, auto-create if not found.
|
|
777
|
+
|
|
778
|
+
Returns:
|
|
779
|
+
Tuple of (physical_sample_id, physical_sample_name).
|
|
780
|
+
"""
|
|
781
|
+
physical_sample = physical_sample.strip()
|
|
782
|
+
if not physical_sample:
|
|
783
|
+
raise ClientError("physical_sample cannot be empty")
|
|
784
|
+
|
|
785
|
+
samples_df = self.list_physical_samples()
|
|
786
|
+
|
|
787
|
+
if self._UUID_RE.match(physical_sample):
|
|
788
|
+
match = samples_df[samples_df["Physical Sample ID"] == physical_sample]
|
|
789
|
+
if match.empty:
|
|
790
|
+
raise ClientError(
|
|
791
|
+
f"Physical sample with id '{physical_sample}' not found"
|
|
792
|
+
)
|
|
793
|
+
return physical_sample, match.iloc[0]["Physical Sample Name"]
|
|
794
|
+
|
|
795
|
+
# Name lookup: case-insensitive exact match
|
|
796
|
+
names_lower = samples_df["Physical Sample Name"].str.strip().str.lower()
|
|
797
|
+
mask = names_lower == physical_sample.lower()
|
|
798
|
+
match = samples_df[mask]
|
|
799
|
+
|
|
800
|
+
if not match.empty:
|
|
801
|
+
return match.iloc[0]["Physical Sample ID"], match.iloc[0][
|
|
802
|
+
"Physical Sample Name"
|
|
803
|
+
]
|
|
804
|
+
|
|
805
|
+
# Not found — create a new physical sample
|
|
806
|
+
resp: dict = self._post_or_put( # type: ignore # noqa: PGH003
|
|
807
|
+
method="POST",
|
|
808
|
+
sub_url="physical_samples/",
|
|
809
|
+
body={"name": physical_sample},
|
|
656
810
|
)
|
|
811
|
+
return resp["id"], physical_sample
|
|
657
812
|
|
|
658
|
-
def upload(
|
|
659
|
-
|
|
813
|
+
def upload(
|
|
814
|
+
self,
|
|
815
|
+
files: list[str | BinaryIO],
|
|
816
|
+
physical_sample: str | None = None,
|
|
817
|
+
) -> list[str]:
|
|
818
|
+
"""Upload and process files.
|
|
660
819
|
|
|
661
820
|
Args:
|
|
662
|
-
files (list[str | BinaryIO]): List containing string paths to files, or BinaryIO objects from
|
|
821
|
+
files (list[str | BinaryIO]): List containing string paths to files, or BinaryIO objects from ``open``.
|
|
822
|
+
physical_sample (str | None): Physical sample name or UUID to link uploads to.
|
|
823
|
+
If a name is given and no matching sample exists, one is created automatically.
|
|
824
|
+
|
|
825
|
+
Returns:
|
|
826
|
+
list[str]: Data IDs assigned to the uploaded files.
|
|
663
827
|
"""
|
|
664
828
|
chunk_size = 40 * 1024 * 1024 # 40 MiB
|
|
665
829
|
|
|
830
|
+
# Resolve physical sample before uploading so we fail fast on bad input
|
|
831
|
+
metadata_body: dict[str, str] | None = None
|
|
832
|
+
if physical_sample is not None:
|
|
833
|
+
ps_id, ps_name = self._resolve_physical_sample(physical_sample)
|
|
834
|
+
metadata_body = {
|
|
835
|
+
"physical_sample_id": ps_id,
|
|
836
|
+
"physical_sample_name": ps_name,
|
|
837
|
+
}
|
|
838
|
+
|
|
666
839
|
# Check to make sure list is valid and get pre-signed URL nums
|
|
667
840
|
file_data = []
|
|
668
841
|
for file in files:
|
|
@@ -697,7 +870,7 @@ class Client(BaseClient):
|
|
|
697
870
|
file_info: dict[
|
|
698
871
|
Literal["num_urls", "file_name", "file_size", "file_path"], int | str
|
|
699
872
|
],
|
|
700
|
-
):
|
|
873
|
+
) -> str:
|
|
701
874
|
url_data: list[dict[str, str | int]] = self._post_or_put(
|
|
702
875
|
method="POST",
|
|
703
876
|
sub_url="data_entries/raw_data/staged/upload_urls/",
|
|
@@ -706,6 +879,7 @@ class Client(BaseClient):
|
|
|
706
879
|
"num_parts": file_info["num_urls"],
|
|
707
880
|
"staging_type": "core",
|
|
708
881
|
},
|
|
882
|
+
body=metadata_body,
|
|
709
883
|
) # type: ignore # noqa: PGH003
|
|
710
884
|
|
|
711
885
|
# Iterate through data structure above and upload file using multi-part S3 urls. Multithread appropriately.
|
|
@@ -783,6 +957,9 @@ class Client(BaseClient):
|
|
|
783
957
|
},
|
|
784
958
|
)
|
|
785
959
|
|
|
960
|
+
return str(first_part["data_id"])
|
|
961
|
+
|
|
962
|
+
data_ids: list[str] = []
|
|
786
963
|
main_task = None
|
|
787
964
|
file_count = len(file_data)
|
|
788
965
|
with _make_progress(self.mute_bars, False) as progress:
|
|
@@ -803,10 +980,12 @@ class Client(BaseClient):
|
|
|
803
980
|
for file_info in file_data
|
|
804
981
|
}
|
|
805
982
|
for future in as_completed(futures):
|
|
806
|
-
future.result()
|
|
983
|
+
data_ids.append(future.result())
|
|
807
984
|
if main_task is not None:
|
|
808
985
|
progress.update(main_task, advance=1, refresh=True)
|
|
809
986
|
|
|
987
|
+
return data_ids
|
|
988
|
+
|
|
810
989
|
def download_videos(
|
|
811
990
|
self,
|
|
812
991
|
data_ids: str | list[str],
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from .changepoint import ChangepointResult
|
|
1
2
|
from .group import PhysicalSampleResult, ProjectResult
|
|
2
3
|
from .metrology import MetrologyResult
|
|
3
4
|
from .optical import OpticalResult
|
|
@@ -8,8 +9,10 @@ from .rheed_video import RHEEDVideoResult
|
|
|
8
9
|
from .similarity_trajectory import SimilarityTrajectoryResult
|
|
9
10
|
from .unknown import UnknownResult
|
|
10
11
|
from .xps import XPSResult
|
|
12
|
+
from .xrd import XRDResult
|
|
11
13
|
|
|
12
14
|
__all__ = [
|
|
15
|
+
"ChangepointResult",
|
|
13
16
|
"MetrologyResult",
|
|
14
17
|
"OpticalResult",
|
|
15
18
|
"PhotoluminescenceResult",
|
|
@@ -22,5 +25,6 @@ __all__ = [
|
|
|
22
25
|
"SimilarityTrajectoryResult",
|
|
23
26
|
"UnknownResult",
|
|
24
27
|
"XPSResult",
|
|
28
|
+
"XRDResult",
|
|
25
29
|
"_get_rheed_image_result",
|
|
26
30
|
]
|