atomscale 0.8.1__tar.gz → 0.8.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 (113) hide show
  1. {atomscale-0.8.1 → atomscale-0.8.2}/PKG-INFO +1 -1
  2. {atomscale-0.8.1 → atomscale-0.8.2}/pyproject.toml +0 -1
  3. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/results/__init__.py +2 -0
  4. atomscale-0.8.2/src/atomscale/results/similarity_trajectory.py +34 -0
  5. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/timeseries/__init__.py +2 -0
  6. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/timeseries/align.py +24 -24
  7. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/timeseries/registry.py +2 -0
  8. atomscale-0.8.2/src/atomscale/timeseries/similarity.py +113 -0
  9. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale.egg-info/PKG-INFO +1 -1
  10. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale.egg-info/SOURCES.txt +3 -0
  11. {atomscale-0.8.1 → atomscale-0.8.2}/tests/conftest.py +2 -0
  12. atomscale-0.8.2/tests/test_similarity_trajectory.py +97 -0
  13. {atomscale-0.8.1 → atomscale-0.8.2}/.github/workflows/release.yml +0 -0
  14. {atomscale-0.8.1 → atomscale-0.8.2}/.github/workflows/testing.yml +0 -0
  15. {atomscale-0.8.1 → atomscale-0.8.2}/.github/workflows/upgrade_dependencies.yml +0 -0
  16. {atomscale-0.8.1 → atomscale-0.8.2}/.gitignore +0 -0
  17. {atomscale-0.8.1 → atomscale-0.8.2}/.pre-commit-config.yaml +0 -0
  18. {atomscale-0.8.1 → atomscale-0.8.2}/CHANGELOG.md +0 -0
  19. {atomscale-0.8.1 → atomscale-0.8.2}/LICENSE +0 -0
  20. {atomscale-0.8.1 → atomscale-0.8.2}/MANIFEST.in +0 -0
  21. {atomscale-0.8.1 → atomscale-0.8.2}/README.md +0 -0
  22. {atomscale-0.8.1 → atomscale-0.8.2}/atomicds-shim-dist/pyproject.toml +0 -0
  23. {atomscale-0.8.1 → atomscale-0.8.2}/docs/Makefile +0 -0
  24. {atomscale-0.8.1 → atomscale-0.8.2}/docs/_templates/custom-class-template.rst +0 -0
  25. {atomscale-0.8.1 → atomscale-0.8.2}/docs/_templates/custom-module-template.rst +0 -0
  26. {atomscale-0.8.1 → atomscale-0.8.2}/docs/conf.py +0 -0
  27. {atomscale-0.8.1 → atomscale-0.8.2}/docs/guides/index.rst +0 -0
  28. {atomscale-0.8.1 → atomscale-0.8.2}/docs/guides/inspect-results.rst +0 -0
  29. {atomscale-0.8.1 → atomscale-0.8.2}/docs/guides/poll-timeseries.rst +0 -0
  30. {atomscale-0.8.1 → atomscale-0.8.2}/docs/guides/quickstart.rst +0 -0
  31. {atomscale-0.8.1 → atomscale-0.8.2}/docs/guides/search-data.rst +0 -0
  32. {atomscale-0.8.1 → atomscale-0.8.2}/docs/guides/stream-rheed.rst +0 -0
  33. {atomscale-0.8.1 → atomscale-0.8.2}/docs/guides/upload-data.rst +0 -0
  34. {atomscale-0.8.1 → atomscale-0.8.2}/docs/index.rst +0 -0
  35. {atomscale-0.8.1 → atomscale-0.8.2}/docs/make.bat +0 -0
  36. {atomscale-0.8.1 → atomscale-0.8.2}/docs/modules.rst +0 -0
  37. {atomscale-0.8.1 → atomscale-0.8.2}/examples/general_use.ipynb +0 -0
  38. {atomscale-0.8.1 → atomscale-0.8.2}/examples/rheed_streaming.ipynb +0 -0
  39. {atomscale-0.8.1 → atomscale-0.8.2}/examples/timeseries_polling.ipynb +0 -0
  40. {atomscale-0.8.1 → atomscale-0.8.2}/examples/vxwse2-placeholder/task1_films.ipynb +0 -0
  41. {atomscale-0.8.1 → atomscale-0.8.2}/examples/vxwse2-placeholder/task1_sapphire.ipynb +0 -0
  42. {atomscale-0.8.1 → atomscale-0.8.2}/examples/vxwse2-placeholder/task2_composition.ipynb +0 -0
  43. {atomscale-0.8.1 → atomscale-0.8.2}/requirements/requirements-macos-latest_py3.10.txt +0 -0
  44. {atomscale-0.8.1 → atomscale-0.8.2}/requirements/requirements-macos-latest_py3.10_extras.txt +0 -0
  45. {atomscale-0.8.1 → atomscale-0.8.2}/requirements/requirements-macos-latest_py3.11.txt +0 -0
  46. {atomscale-0.8.1 → atomscale-0.8.2}/requirements/requirements-macos-latest_py3.11_extras.txt +0 -0
  47. {atomscale-0.8.1 → atomscale-0.8.2}/requirements/requirements-macos-latest_py3.12.txt +0 -0
  48. {atomscale-0.8.1 → atomscale-0.8.2}/requirements/requirements-macos-latest_py3.12_extras.txt +0 -0
  49. {atomscale-0.8.1 → atomscale-0.8.2}/requirements/requirements-macos-latest_py3.9.txt +0 -0
  50. {atomscale-0.8.1 → atomscale-0.8.2}/requirements/requirements-macos-latest_py3.9_extras.txt +0 -0
  51. {atomscale-0.8.1 → atomscale-0.8.2}/requirements/requirements-ubuntu-latest_py3.10.txt +0 -0
  52. {atomscale-0.8.1 → atomscale-0.8.2}/requirements/requirements-ubuntu-latest_py3.10_extras.txt +0 -0
  53. {atomscale-0.8.1 → atomscale-0.8.2}/requirements/requirements-ubuntu-latest_py3.11.txt +0 -0
  54. {atomscale-0.8.1 → atomscale-0.8.2}/requirements/requirements-ubuntu-latest_py3.11_extras.txt +0 -0
  55. {atomscale-0.8.1 → atomscale-0.8.2}/requirements/requirements-ubuntu-latest_py3.12.txt +0 -0
  56. {atomscale-0.8.1 → atomscale-0.8.2}/requirements/requirements-ubuntu-latest_py3.12_extras.txt +0 -0
  57. {atomscale-0.8.1 → atomscale-0.8.2}/requirements/requirements-ubuntu-latest_py3.9.txt +0 -0
  58. {atomscale-0.8.1 → atomscale-0.8.2}/requirements/requirements-ubuntu-latest_py3.9_extras.txt +0 -0
  59. {atomscale-0.8.1 → atomscale-0.8.2}/requirements/requirements-windows-latest_py3.10.txt +0 -0
  60. {atomscale-0.8.1 → atomscale-0.8.2}/requirements/requirements-windows-latest_py3.10_extras.txt +0 -0
  61. {atomscale-0.8.1 → atomscale-0.8.2}/requirements/requirements-windows-latest_py3.11.txt +0 -0
  62. {atomscale-0.8.1 → atomscale-0.8.2}/requirements/requirements-windows-latest_py3.11_extras.txt +0 -0
  63. {atomscale-0.8.1 → atomscale-0.8.2}/requirements/requirements-windows-latest_py3.12.txt +0 -0
  64. {atomscale-0.8.1 → atomscale-0.8.2}/requirements/requirements-windows-latest_py3.12_extras.txt +0 -0
  65. {atomscale-0.8.1 → atomscale-0.8.2}/requirements/requirements-windows-latest_py3.9.txt +0 -0
  66. {atomscale-0.8.1 → atomscale-0.8.2}/requirements/requirements-windows-latest_py3.9_extras.txt +0 -0
  67. {atomscale-0.8.1 → atomscale-0.8.2}/setup.cfg +0 -0
  68. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomicds/__init__.py +0 -0
  69. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/__init__.py +0 -0
  70. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/client.py +0 -0
  71. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/core/__init__.py +0 -0
  72. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/core/client.py +0 -0
  73. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/core/files.py +0 -0
  74. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/core/utils.py +0 -0
  75. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/results/group.py +0 -0
  76. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/results/metrology.py +0 -0
  77. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/results/optical.py +0 -0
  78. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/results/photoluminescence.py +0 -0
  79. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/results/raman.py +0 -0
  80. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/results/rheed_image.py +0 -0
  81. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/results/rheed_video.py +0 -0
  82. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/results/unknown.py +0 -0
  83. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/results/xps.py +0 -0
  84. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/streaming/Cargo.lock +0 -0
  85. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/streaming/Cargo.toml +0 -0
  86. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/streaming/__init__.py +0 -0
  87. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/streaming/rheed_stream.pyi +0 -0
  88. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/streaming/src/initialize.rs +0 -0
  89. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/streaming/src/lib.rs +0 -0
  90. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/streaming/src/upload.rs +0 -0
  91. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/streaming/src/utils.rs +0 -0
  92. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/timeseries/metrology.py +0 -0
  93. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/timeseries/optical.py +0 -0
  94. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/timeseries/polling.py +0 -0
  95. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/timeseries/provider.py +0 -0
  96. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/timeseries/rheed.py +0 -0
  97. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale/timeseries/sample.py +0 -0
  98. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale.egg-info/dependency_links.txt +0 -0
  99. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale.egg-info/requires.txt +0 -0
  100. {atomscale-0.8.1 → atomscale-0.8.2}/src/atomscale.egg-info/top_level.txt +0 -0
  101. {atomscale-0.8.1 → atomscale-0.8.2}/tests/__init__.py +0 -0
  102. {atomscale-0.8.1 → atomscale-0.8.2}/tests/data/test_rheed.mp4 +0 -0
  103. {atomscale-0.8.1 → atomscale-0.8.2}/tests/test_atomicds_alias.py +0 -0
  104. {atomscale-0.8.1 → atomscale-0.8.2}/tests/test_client.py +0 -0
  105. {atomscale-0.8.1 → atomscale-0.8.2}/tests/test_core.py +0 -0
  106. {atomscale-0.8.1 → atomscale-0.8.2}/tests/test_metrology.py +0 -0
  107. {atomscale-0.8.1 → atomscale-0.8.2}/tests/test_optical.py +0 -0
  108. {atomscale-0.8.1 → atomscale-0.8.2}/tests/test_photoluminescence.py +0 -0
  109. {atomscale-0.8.1 → atomscale-0.8.2}/tests/test_polling.py +0 -0
  110. {atomscale-0.8.1 → atomscale-0.8.2}/tests/test_raman.py +0 -0
  111. {atomscale-0.8.1 → atomscale-0.8.2}/tests/test_rheed_image.py +0 -0
  112. {atomscale-0.8.1 → atomscale-0.8.2}/tests/test_rheed_video.py +0 -0
  113. {atomscale-0.8.1 → atomscale-0.8.2}/tests/test_xps.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atomscale
3
- Version: 0.8.1
3
+ Version: 0.8.2
4
4
  Summary: Python SDK for Atomscale.
5
5
  Author-email: Atomscale <info@atomscale.ai>
6
6
  License: GPL-3.0-only
@@ -125,7 +125,6 @@ lint.extend-ignore = [
125
125
  "B028", # No explicit stacklevel
126
126
  "EM101", # Exception must not use a string literal
127
127
  "EM102", # Exception must not use an f-string literal
128
- "PD901", # Avoid using the generic variable name `df` for DataFrames
129
128
  ]
130
129
  lint.typing-modules = ["mypackage._compat.typing"]
131
130
  src = ["src"]
@@ -5,6 +5,7 @@ from .photoluminescence import PhotoluminescenceResult
5
5
  from .raman import RamanResult
6
6
  from .rheed_image import RHEEDImageCollection, RHEEDImageResult, _get_rheed_image_result
7
7
  from .rheed_video import RHEEDVideoResult
8
+ from .similarity_trajectory import SimilarityTrajectoryResult
8
9
  from .unknown import UnknownResult
9
10
  from .xps import XPSResult
10
11
 
@@ -18,6 +19,7 @@ __all__ = [
18
19
  "RHEEDImageResult",
19
20
  "RHEEDVideoResult",
20
21
  "RamanResult",
22
+ "SimilarityTrajectoryResult",
21
23
  "UnknownResult",
22
24
  "XPSResult",
23
25
  "_get_rheed_image_result",
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from uuid import UUID
5
+
6
+ from monty.json import MSONable
7
+ from pandas import DataFrame
8
+
9
+
10
+ class SimilarityTrajectoryResult(MSONable):
11
+ def __init__(
12
+ self,
13
+ source_id: UUID | str,
14
+ workflow: str,
15
+ window_span: float,
16
+ timeseries_data: DataFrame,
17
+ source_data_ids: Sequence[UUID | str] | None = None,
18
+ ):
19
+ """Similarity trajectory result
20
+
21
+ Args:
22
+ source_id (UUID | str): Source ID for the similarity trajectory query.
23
+ workflow (str): Workflow name used for the similarity analysis.
24
+ window_span (float): Window span parameter used for the trajectory.
25
+ timeseries_data (DataFrame): Pandas DataFrame with similarity trajectory data.
26
+ source_data_ids (Sequence[UUID | str] | None): Sequence of source data IDs included in the trajectory.
27
+ """
28
+ self.source_id = source_id
29
+ self.workflow = workflow
30
+ self.window_span = window_span
31
+ self.timeseries_data = timeseries_data
32
+ self.source_data_ids: list[UUID | str] = (
33
+ list(source_data_ids) if source_data_ids else []
34
+ )
@@ -4,11 +4,13 @@ from .optical import OpticalProvider
4
4
  from .provider import TimeseriesProvider
5
5
  from .registry import get_provider
6
6
  from .rheed import RHEEDProvider
7
+ from .similarity import SimilarityTrajectoryProvider
7
8
 
8
9
  __all__ = [
9
10
  "MetrologyProvider",
10
11
  "OpticalProvider",
11
12
  "RHEEDProvider",
13
+ "SimilarityTrajectoryProvider",
12
14
  "TimeseriesProvider",
13
15
  "align_timeseries",
14
16
  "get_provider",
@@ -71,26 +71,26 @@ def _extract_timeseries(result):
71
71
  """Return (data_id, domain, df_with_timeindex) or None for non-timeseries."""
72
72
  if isinstance(result, RHEEDVideoResult):
73
73
  domain = "rheed"
74
- df = result.timeseries_data
74
+ timeseries = result.timeseries_data
75
75
  elif isinstance(result, OpticalResult):
76
76
  domain = "optical"
77
- df = result.timeseries_data
77
+ timeseries = result.timeseries_data
78
78
  elif isinstance(result, MetrologyResult):
79
79
  domain = "metrology"
80
- df = result.timeseries_data
80
+ timeseries = result.timeseries_data
81
81
  else:
82
82
  return None
83
83
 
84
- if df is None or df.empty:
84
+ if timeseries is None or timeseries.empty:
85
85
  return None
86
86
 
87
87
  # Build time index: prefer absolute epochs; fall back to upload_datetime + relative offsets.
88
88
  upload_dt = getattr(result, "upload_datetime", None)
89
89
 
90
- time_index = _infer_absolute_time(df)
90
+ time_index = _infer_absolute_time(timeseries)
91
91
  if time_index is None and upload_dt is not None:
92
92
  base = pd.to_datetime(upload_dt, utc=True, errors="coerce")
93
- rel = _infer_relative_time(df)
93
+ rel = _infer_relative_time(timeseries)
94
94
  if base is not pd.NaT and rel is not None:
95
95
  time_index = base + rel
96
96
 
@@ -101,7 +101,7 @@ def _extract_timeseries(result):
101
101
  if not valid_mask.any():
102
102
  return None
103
103
 
104
- indexed = df.loc[valid_mask].copy(deep=False)
104
+ indexed = timeseries.loc[valid_mask].copy(deep=False)
105
105
  indexed.index = pd.Index(time_index[valid_mask], name="time")
106
106
  indexed = indexed.sort_index()
107
107
 
@@ -173,11 +173,11 @@ def align_timeseries(
173
173
  if not extracted:
174
174
  continue
175
175
 
176
- data_id, domain, df = extracted
177
- df = df.copy(deep=False)
178
- df.columns = pd.MultiIndex.from_product([[data_id], [domain], df.columns])
179
- frames.append(df)
180
- indices.append(df.index)
176
+ data_id, domain, frame = extracted
177
+ frame = frame.copy(deep=False)
178
+ frame.columns = pd.MultiIndex.from_product([[data_id], [domain], frame.columns])
179
+ frames.append(frame)
180
+ indices.append(frame.index)
181
181
 
182
182
  if not frames:
183
183
  return pd.DataFrame()
@@ -211,11 +211,11 @@ def align_timeseries(
211
211
 
212
212
  # Merge compatible metrics across items: if multiple columns share (domain, metric)
213
213
  # and never conflict where they overlap, collapse into (shared, domain, metric).
214
- def _merge_compatible_metrics(df: pd.DataFrame) -> pd.DataFrame:
215
- if not isinstance(df.columns, pd.MultiIndex):
216
- return df
217
- domains = df.columns.get_level_values(1)
218
- metrics = df.columns.get_level_values(2)
214
+ def _merge_compatible_metrics(data: pd.DataFrame) -> pd.DataFrame:
215
+ if not isinstance(data.columns, pd.MultiIndex):
216
+ return data
217
+ domains = data.columns.get_level_values(1)
218
+ metrics = data.columns.get_level_values(2)
219
219
  new_cols: dict = {}
220
220
  drop_cols: list = []
221
221
 
@@ -223,16 +223,16 @@ def align_timeseries(
223
223
  for metric in metrics.unique():
224
224
  cols = [
225
225
  c
226
- for c in df.columns
226
+ for c in data.columns
227
227
  if c[1] == domain and c[2] == metric and c[0] != "shared"
228
228
  ]
229
229
  if len(cols) <= 1:
230
230
  continue
231
231
 
232
- merged = df[cols[0]]
232
+ merged = data[cols[0]]
233
233
  conflict = False
234
234
  for c in cols[1:]:
235
- other = df[c]
235
+ other = data[c]
236
236
  overlap_mask = merged.notna() & other.notna()
237
237
  if (merged[overlap_mask] != other[overlap_mask]).any():
238
238
  conflict = True
@@ -247,10 +247,10 @@ def align_timeseries(
247
247
  drop_cols.extend(cols)
248
248
 
249
249
  if new_cols:
250
- df = df.drop(columns=drop_cols)
250
+ data = data.drop(columns=drop_cols)
251
251
  for col, series in new_cols.items():
252
- df[col] = series
253
- df = df.sort_index(axis=1)
254
- return df
252
+ data[col] = series
253
+ data = data.sort_index(axis=1)
254
+ return data
255
255
 
256
256
  return _merge_compatible_metrics(aligned)
@@ -4,11 +4,13 @@ from .metrology import MetrologyProvider
4
4
  from .optical import OpticalProvider
5
5
  from .provider import TimeseriesProvider
6
6
  from .rheed import RHEEDProvider
7
+ from .similarity import SimilarityTrajectoryProvider
7
8
 
8
9
  _PROVIDER_CLASSES: dict[str, type[TimeseriesProvider]] = {
9
10
  RHEEDProvider.TYPE: RHEEDProvider,
10
11
  OpticalProvider.TYPE: OpticalProvider,
11
12
  MetrologyProvider.TYPE: MetrologyProvider,
13
+ SimilarityTrajectoryProvider.TYPE: SimilarityTrajectoryProvider,
12
14
  }
13
15
 
14
16
 
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping, Sequence
4
+ from typing import Any
5
+ from uuid import UUID
6
+
7
+ from pandas import DataFrame, concat
8
+
9
+ from atomscale.core import BaseClient
10
+ from atomscale.results.similarity_trajectory import SimilarityTrajectoryResult
11
+ from atomscale.timeseries.provider import TimeseriesProvider
12
+
13
+
14
+ class SimilarityTrajectoryProvider(TimeseriesProvider[SimilarityTrajectoryResult]):
15
+ TYPE = "similarity_trajectory"
16
+
17
+ RENAME_MAP: Mapping[str, str] = {
18
+ "reference_id": "Reference ID",
19
+ "reference_item_name": "Reference Name",
20
+ "real_time_seconds": "Time",
21
+ "similarity_values": "Similarity",
22
+ "unix_times": "UNIX Timestamp",
23
+ "is_active": "Active",
24
+ "averaged_count": "Averaged Count",
25
+ }
26
+ INDEX_COLS: Sequence[str] = ["Reference ID", "Time"]
27
+
28
+ def fetch_raw(self, client: BaseClient, data_id: str, **kwargs: Any) -> Any:
29
+ """Fetch similarity trajectory data from the API.
30
+
31
+ Args:
32
+ client: The API client.
33
+ data_id: The source ID for the similarity query.
34
+ **kwargs: Must include 'workflow' (required). Optional parameters:
35
+ window_span, reference_ids, softmax_mode, reference_n_values.
36
+
37
+ Returns:
38
+ Raw API response payload.
39
+
40
+ Raises:
41
+ KeyError: If 'workflow' is not provided in kwargs.
42
+ """
43
+ workflow = kwargs.pop("workflow")
44
+ return client._get(
45
+ sub_url=f"similarity/{workflow}/{data_id}/trajectory/",
46
+ params=kwargs,
47
+ )
48
+
49
+ def to_dataframe(self, raw: Any) -> DataFrame:
50
+ if not raw:
51
+ return DataFrame(None)
52
+
53
+ trajectories = raw.get("trajectories", [])
54
+ if not trajectories:
55
+ return DataFrame(None)
56
+
57
+ frames: list[DataFrame] = []
58
+ for traj in trajectories:
59
+ ref_id = traj.get("reference_id")
60
+ ref_name = traj.get("reference_item_name")
61
+ similarity_values = traj.get("similarity_values", [])
62
+ real_time_seconds = traj.get("real_time_seconds", [])
63
+ unix_times = traj.get("unix_times", [])
64
+ is_active = traj.get("is_active")
65
+ averaged_count = traj.get("averaged_count")
66
+
67
+ if not similarity_values:
68
+ continue
69
+
70
+ # Build dataframe from columnar data
71
+ traj_df = DataFrame(
72
+ {
73
+ "reference_id": ref_id,
74
+ "reference_item_name": ref_name,
75
+ "similarity_values": similarity_values,
76
+ "real_time_seconds": real_time_seconds,
77
+ "unix_times": unix_times,
78
+ "is_active": is_active,
79
+ "averaged_count": averaged_count,
80
+ }
81
+ )
82
+ frames.append(traj_df)
83
+
84
+ if not frames:
85
+ return DataFrame(None)
86
+
87
+ df_all = concat(frames, axis=0, ignore_index=True)
88
+ df_all = df_all.rename(columns=self.RENAME_MAP)
89
+
90
+ idx_cols = [c for c in self.INDEX_COLS if c in df_all.columns]
91
+ if idx_cols:
92
+ df_all = df_all.set_index(idx_cols)
93
+
94
+ return df_all
95
+
96
+ def build_result(
97
+ self,
98
+ client: BaseClient, # noqa: ARG002
99
+ data_id: str,
100
+ data_type: str, # noqa: ARG002
101
+ ts_df: DataFrame,
102
+ *,
103
+ workflow: str = "",
104
+ window_span: float = 0.0,
105
+ source_data_ids: Sequence[UUID | str] | None = None,
106
+ ) -> SimilarityTrajectoryResult:
107
+ return SimilarityTrajectoryResult(
108
+ source_id=data_id,
109
+ workflow=workflow,
110
+ window_span=window_span,
111
+ timeseries_data=ts_df,
112
+ source_data_ids=source_data_ids,
113
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atomscale
3
- Version: 0.8.1
3
+ Version: 0.8.2
4
4
  Summary: Python SDK for Atomscale.
5
5
  Author-email: Atomscale <info@atomscale.ai>
6
6
  License: GPL-3.0-only
@@ -73,6 +73,7 @@ src/atomscale/results/photoluminescence.py
73
73
  src/atomscale/results/raman.py
74
74
  src/atomscale/results/rheed_image.py
75
75
  src/atomscale/results/rheed_video.py
76
+ src/atomscale/results/similarity_trajectory.py
76
77
  src/atomscale/results/unknown.py
77
78
  src/atomscale/results/xps.py
78
79
  src/atomscale/streaming/Cargo.lock
@@ -92,6 +93,7 @@ src/atomscale/timeseries/provider.py
92
93
  src/atomscale/timeseries/registry.py
93
94
  src/atomscale/timeseries/rheed.py
94
95
  src/atomscale/timeseries/sample.py
96
+ src/atomscale/timeseries/similarity.py
95
97
  tests/__init__.py
96
98
  tests/conftest.py
97
99
  tests/test_atomicds_alias.py
@@ -104,5 +106,6 @@ tests/test_polling.py
104
106
  tests/test_raman.py
105
107
  tests/test_rheed_image.py
106
108
  tests/test_rheed_video.py
109
+ tests/test_similarity_trajectory.py
107
110
  tests/test_xps.py
108
111
  tests/data/test_rheed.mp4
@@ -27,3 +27,5 @@ class ResultIDs:
27
27
  metrology = ""
28
28
  photoluminescence = ""
29
29
  raman = ""
30
+ similarity_workflow = "rheed_stationary"
31
+ similarity_source_id = "bb3494b1-b5fb-4f3e-ac50-e4024f8aacf5"
@@ -0,0 +1,97 @@
1
+ import pytest
2
+ from pandas import DataFrame
3
+
4
+ from atomscale import Client
5
+ from atomscale.results import SimilarityTrajectoryResult
6
+ from atomscale.timeseries.similarity import SimilarityTrajectoryProvider
7
+
8
+ from .conftest import ResultIDs
9
+
10
+
11
+ @pytest.fixture
12
+ def client():
13
+ return Client()
14
+
15
+
16
+ @pytest.fixture
17
+ def provider():
18
+ return SimilarityTrajectoryProvider()
19
+
20
+
21
+ @pytest.fixture
22
+ def raw_data(client: Client, provider: SimilarityTrajectoryProvider):
23
+ if not ResultIDs.similarity_workflow or not ResultIDs.similarity_source_id:
24
+ pytest.skip("No similarity trajectory data available")
25
+
26
+ data = provider.fetch_raw(
27
+ client,
28
+ ResultIDs.similarity_source_id,
29
+ workflow=ResultIDs.similarity_workflow,
30
+ )
31
+
32
+ if not data or not data.get("trajectories"):
33
+ pytest.skip("No trajectory data returned from API")
34
+
35
+ return data
36
+
37
+
38
+ @pytest.fixture
39
+ def result(
40
+ client: Client, provider: SimilarityTrajectoryProvider, raw_data: dict
41
+ ) -> SimilarityTrajectoryResult:
42
+ df = provider.to_dataframe(raw_data)
43
+ return provider.build_result(
44
+ client=client,
45
+ data_id=ResultIDs.similarity_source_id,
46
+ data_type="similarity_trajectory",
47
+ ts_df=df,
48
+ workflow=ResultIDs.similarity_workflow,
49
+ )
50
+
51
+
52
+ def test_type_constant():
53
+ """Verify TYPE constant is set correctly."""
54
+ assert SimilarityTrajectoryProvider.TYPE == "similarity_trajectory"
55
+
56
+
57
+ def test_fetch_raw(raw_data: dict):
58
+ """Verify raw data is fetched from API."""
59
+ assert raw_data is not None
60
+ assert "trajectories" in raw_data
61
+
62
+
63
+ def test_to_dataframe(provider: SimilarityTrajectoryProvider, raw_data: dict):
64
+ """Verify dataframe conversion."""
65
+ df = provider.to_dataframe(raw_data)
66
+
67
+ assert isinstance(df, DataFrame)
68
+ assert not df.empty
69
+
70
+
71
+ def test_to_dataframe_columns(provider: SimilarityTrajectoryProvider, raw_data: dict):
72
+ """Verify column names and index."""
73
+ df = provider.to_dataframe(raw_data)
74
+
75
+ # Check index names
76
+ assert df.index.names == ["Reference ID", "Time"]
77
+
78
+ # Check expected columns exist
79
+ expected_columns = {"Similarity", "Reference Name", "UNIX Timestamp", "Active", "Averaged Count"}
80
+ assert expected_columns == set(df.columns)
81
+
82
+
83
+ def test_build_result(result: SimilarityTrajectoryResult):
84
+ """Verify result object construction."""
85
+ assert isinstance(result, SimilarityTrajectoryResult)
86
+ assert result.source_id == ResultIDs.similarity_source_id
87
+ assert result.workflow == ResultIDs.similarity_workflow
88
+ assert isinstance(result.timeseries_data, DataFrame)
89
+
90
+
91
+ def test_result_dataframe(result: SimilarityTrajectoryResult):
92
+ """Verify result contains valid timeseries data."""
93
+ df = result.timeseries_data
94
+
95
+ assert isinstance(df, DataFrame)
96
+ if df.index.names != [None]:
97
+ assert df.index.names == ["Reference ID", "Time"]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes