atomscale 0.7.0__tar.gz → 0.7.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.
- {atomscale-0.7.0/src/atomscale.egg-info → atomscale-0.7.2}/PKG-INFO +1 -1
- {atomscale-0.7.0 → atomscale-0.7.2}/docs/index.rst +4 -4
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/client.py +16 -30
- atomscale-0.7.2/src/atomscale/timeseries/align.py +256 -0
- {atomscale-0.7.0 → atomscale-0.7.2/src/atomscale.egg-info}/PKG-INFO +1 -1
- atomscale-0.7.0/src/atomscale/timeseries/align.py +0 -106
- {atomscale-0.7.0 → atomscale-0.7.2}/.github/workflows/release.yml +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/.github/workflows/testing.yml +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/.github/workflows/upgrade_dependencies.yml +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/.gitignore +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/.pre-commit-config.yaml +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/CHANGELOG.md +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/LICENSE +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/MANIFEST.in +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/README.md +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/atomicds-shim-dist/pyproject.toml +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/docs/Makefile +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/docs/_templates/custom-class-template.rst +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/docs/_templates/custom-module-template.rst +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/docs/conf.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/docs/guides/index.rst +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/docs/guides/inspect-results.rst +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/docs/guides/poll-timeseries.rst +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/docs/guides/quickstart.rst +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/docs/guides/search-data.rst +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/docs/guides/stream-rheed.rst +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/docs/guides/upload-data.rst +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/docs/make.bat +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/docs/modules.rst +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/examples/general_use.ipynb +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/examples/rheed_streaming.ipynb +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/examples/timeseries_polling.ipynb +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/examples/vxwse2-placeholder/task1_films.ipynb +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/examples/vxwse2-placeholder/task1_sapphire.ipynb +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/examples/vxwse2-placeholder/task2_composition.ipynb +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/pyproject.toml +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-macos-latest_py3.10.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-macos-latest_py3.10_extras.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-macos-latest_py3.11.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-macos-latest_py3.11_extras.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-macos-latest_py3.12.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-macos-latest_py3.12_extras.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-macos-latest_py3.9.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-macos-latest_py3.9_extras.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-ubuntu-latest_py3.10.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-ubuntu-latest_py3.10_extras.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-ubuntu-latest_py3.11.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-ubuntu-latest_py3.11_extras.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-ubuntu-latest_py3.12.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-ubuntu-latest_py3.12_extras.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-ubuntu-latest_py3.9.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-ubuntu-latest_py3.9_extras.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-windows-latest_py3.10.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-windows-latest_py3.10_extras.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-windows-latest_py3.11.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-windows-latest_py3.11_extras.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-windows-latest_py3.12.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-windows-latest_py3.12_extras.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-windows-latest_py3.9.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-windows-latest_py3.9_extras.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/setup.cfg +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomicds/__init__.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/__init__.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/core/__init__.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/core/client.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/core/files.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/core/utils.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/results/__init__.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/results/group.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/results/metrology.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/results/optical.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/results/photoluminescence.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/results/raman.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/results/rheed_image.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/results/rheed_video.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/results/unknown.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/results/xps.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/streaming/Cargo.lock +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/streaming/Cargo.toml +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/streaming/__init__.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/streaming/rheed_stream.pyi +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/streaming/src/initialize.rs +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/streaming/src/lib.rs +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/streaming/src/upload.rs +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/streaming/src/utils.rs +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/timeseries/__init__.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/timeseries/metrology.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/timeseries/optical.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/timeseries/polling.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/timeseries/provider.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/timeseries/registry.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/timeseries/rheed.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale/timeseries/sample.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale.egg-info/SOURCES.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale.egg-info/dependency_links.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale.egg-info/requires.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/src/atomscale.egg-info/top_level.txt +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/tests/__init__.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/tests/conftest.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/tests/data/test_rheed.mp4 +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/tests/test_atomicds_alias.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/tests/test_client.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/tests/test_core.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/tests/test_metrology.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/tests/test_optical.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/tests/test_photoluminescence.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/tests/test_polling.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/tests/test_raman.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/tests/test_rheed_image.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/tests/test_rheed_video.py +0 -0
- {atomscale-0.7.0 → atomscale-0.7.2}/tests/test_xps.py +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
Atomscale Python SDK
|
|
2
2
|
===================
|
|
3
3
|
|
|
4
4
|
.. |testing-badge| image:: https://github.com/atomscale-ai/sdk/workflows/Testing/badge.svg
|
|
@@ -42,9 +42,9 @@ Installation
|
|
|
42
42
|
|
|
43
43
|
pip install atomscale
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
before creating a :class:`~atomscale.client.Client`,
|
|
47
|
-
constructing the client in your
|
|
45
|
+
Much of the functionality of the package requires an API key. It can be provided using
|
|
46
|
+
the ``AS_API_KEY`` environment variable before creating a :class:`~atomscale.client.Client`,
|
|
47
|
+
or passed in directly when constructing the client in your code.
|
|
48
48
|
|
|
49
49
|
.. note::
|
|
50
50
|
|
|
@@ -434,7 +434,6 @@ class Client(BaseClient):
|
|
|
434
434
|
*,
|
|
435
435
|
include_organization_data: bool = True,
|
|
436
436
|
align: bool | str = False,
|
|
437
|
-
resample: str | None = None,
|
|
438
437
|
) -> PhysicalSampleResult:
|
|
439
438
|
"""Get all data for a physical sample.
|
|
440
439
|
|
|
@@ -442,7 +441,6 @@ class Client(BaseClient):
|
|
|
442
441
|
physical_sample_id: Identifier of the physical sample.
|
|
443
442
|
include_organization_data: Whether to include organization data. Defaults to True.
|
|
444
443
|
align: Whether to align timeseries data. If truthy, an aligned DataFrame is returned.
|
|
445
|
-
resample: Optional pandas resample rule applied after alignment.
|
|
446
444
|
"""
|
|
447
445
|
physical_samples: list[dict] | None = self._get( # type: ignore # noqa: PGH003
|
|
448
446
|
sub_url="physical_samples/",
|
|
@@ -468,11 +466,7 @@ class Client(BaseClient):
|
|
|
468
466
|
if isinstance(align, str):
|
|
469
467
|
join_how = align
|
|
470
468
|
|
|
471
|
-
ts_aligned = (
|
|
472
|
-
align_timeseries(results, how=join_how, resample=resample)
|
|
473
|
-
if align
|
|
474
|
-
else None
|
|
475
|
-
)
|
|
469
|
+
ts_aligned = align_timeseries(results, how=join_how) if align else None
|
|
476
470
|
|
|
477
471
|
non_timeseries = [
|
|
478
472
|
r
|
|
@@ -501,7 +495,6 @@ class Client(BaseClient):
|
|
|
501
495
|
*,
|
|
502
496
|
include_organization_data: bool = True,
|
|
503
497
|
align: bool | str = False,
|
|
504
|
-
resample: str | None = None,
|
|
505
498
|
) -> ProjectResult:
|
|
506
499
|
"""Get all data grouped by physical sample for a project.
|
|
507
500
|
|
|
@@ -509,7 +502,6 @@ class Client(BaseClient):
|
|
|
509
502
|
project_id: Identifier of the project.
|
|
510
503
|
include_organization_data: Whether to include organization data. Defaults to True.
|
|
511
504
|
align: Whether to align timeseries at the project level. Defaults to False.
|
|
512
|
-
resample: Optional pandas resample rule applied after alignment.
|
|
513
505
|
"""
|
|
514
506
|
# Get physical samples associated with the project, then fetch data per sample.
|
|
515
507
|
project_samples: list[dict] = (
|
|
@@ -519,39 +511,27 @@ class Client(BaseClient):
|
|
|
519
511
|
return ProjectResult(project_id, None, [], None)
|
|
520
512
|
|
|
521
513
|
sample_results: list[PhysicalSampleResult] = []
|
|
514
|
+
all_results: list = []
|
|
522
515
|
for sample in project_samples:
|
|
523
516
|
sid = sample.get("id")
|
|
524
517
|
if not sid:
|
|
525
518
|
continue
|
|
519
|
+
# For project-level alignment we align once across all entries, so
|
|
520
|
+
# skip per-sample alignment when align=True.
|
|
521
|
+
sample_align = False if align else align
|
|
526
522
|
sample_results.append(
|
|
527
523
|
self.get_physical_sample(
|
|
528
524
|
sid,
|
|
529
525
|
include_organization_data=include_organization_data,
|
|
530
|
-
align=
|
|
531
|
-
resample=resample,
|
|
526
|
+
align=sample_align,
|
|
532
527
|
)
|
|
533
528
|
)
|
|
529
|
+
if sample_results[-1].data_results:
|
|
530
|
+
all_results.extend(sample_results[-1].data_results)
|
|
534
531
|
|
|
535
532
|
project_aligned = None
|
|
536
533
|
if align:
|
|
537
|
-
|
|
538
|
-
for sample in sample_results:
|
|
539
|
-
if sample.aligned_timeseries is None:
|
|
540
|
-
continue
|
|
541
|
-
renamed = sample.aligned_timeseries.copy()
|
|
542
|
-
renamed.columns = pd.MultiIndex.from_tuples(
|
|
543
|
-
[
|
|
544
|
-
(sample.physical_sample_id, *tuple(col))
|
|
545
|
-
if isinstance(col, tuple)
|
|
546
|
-
else (sample.physical_sample_id, col)
|
|
547
|
-
for col in renamed.columns
|
|
548
|
-
]
|
|
549
|
-
)
|
|
550
|
-
frames.append(renamed)
|
|
551
|
-
if frames:
|
|
552
|
-
project_aligned = frames[0]
|
|
553
|
-
for frame in frames[1:]:
|
|
554
|
-
project_aligned = project_aligned.join(frame, how="outer")
|
|
534
|
+
project_aligned = align_timeseries(all_results, how="outer")
|
|
555
535
|
|
|
556
536
|
project_name = None
|
|
557
537
|
return ProjectResult(
|
|
@@ -654,7 +634,13 @@ class Client(BaseClient):
|
|
|
654
634
|
raw = provider.fetch_raw(self, data_id)
|
|
655
635
|
ts_df = provider.to_dataframe(raw)
|
|
656
636
|
|
|
657
|
-
|
|
637
|
+
result_obj = provider.build_result(self, data_id, data_type, ts_df)
|
|
638
|
+
if catalogue_entry:
|
|
639
|
+
# Store upload datetime for alignment fallback when only relative time is available.
|
|
640
|
+
upload_dt = catalogue_entry.get("upload_datetime")
|
|
641
|
+
if upload_dt:
|
|
642
|
+
result_obj.upload_datetime = upload_dt
|
|
643
|
+
return result_obj
|
|
658
644
|
|
|
659
645
|
# Fallback for unknown/unsupported data types
|
|
660
646
|
return UnknownResult(
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
from atomscale.results import MetrologyResult, OpticalResult, RHEEDVideoResult
|
|
8
|
+
|
|
9
|
+
ABS_TIME_COLS = (
|
|
10
|
+
"UNIX Timestamp",
|
|
11
|
+
"Unix Timestamp",
|
|
12
|
+
"unix_timestamp_ms",
|
|
13
|
+
"unix_timestamp",
|
|
14
|
+
"timestamp_ms",
|
|
15
|
+
"timestamp_seconds",
|
|
16
|
+
"timestamp",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
REL_TIME_COLS = (
|
|
20
|
+
"time_seconds",
|
|
21
|
+
"relative_time_seconds",
|
|
22
|
+
"Relative Time",
|
|
23
|
+
"Time",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _infer_absolute_time(df: pd.DataFrame) -> pd.Series | None:
|
|
28
|
+
"""Return UTC datetime index from absolute (epoch-based) columns."""
|
|
29
|
+
for col in ABS_TIME_COLS:
|
|
30
|
+
if col not in df.columns:
|
|
31
|
+
continue
|
|
32
|
+
series = df[col]
|
|
33
|
+
|
|
34
|
+
# Accept numeric-looking strings as well as numeric dtypes
|
|
35
|
+
numeric_series = pd.to_numeric(series, errors="coerce")
|
|
36
|
+
has_numeric = numeric_series.notna().any()
|
|
37
|
+
|
|
38
|
+
target = numeric_series if has_numeric else series
|
|
39
|
+
|
|
40
|
+
if pd.api.types.is_integer_dtype(target) or has_numeric:
|
|
41
|
+
max_val = target.max(skipna=True)
|
|
42
|
+
if max_val > 1e18:
|
|
43
|
+
unit = "ns"
|
|
44
|
+
elif max_val > 1e15:
|
|
45
|
+
unit = "us"
|
|
46
|
+
elif max_val > 1e12:
|
|
47
|
+
unit = "ms"
|
|
48
|
+
else:
|
|
49
|
+
unit = "s"
|
|
50
|
+
return pd.to_datetime(target, unit=unit, errors="coerce", utc=True)
|
|
51
|
+
|
|
52
|
+
if pd.api.types.is_float_dtype(target):
|
|
53
|
+
return pd.to_datetime(target, unit="s", errors="coerce", utc=True)
|
|
54
|
+
|
|
55
|
+
# assume already datetime-like strings
|
|
56
|
+
return pd.to_datetime(target, errors="coerce", utc=True)
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _infer_relative_time(df: pd.DataFrame) -> pd.Series | None:
|
|
61
|
+
"""Return timedelta series from relative time columns (seconds)."""
|
|
62
|
+
for col in REL_TIME_COLS:
|
|
63
|
+
if col not in df.columns:
|
|
64
|
+
continue
|
|
65
|
+
series = df[col]
|
|
66
|
+
return pd.to_timedelta(series, unit="s", errors="coerce")
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _extract_timeseries(result):
|
|
71
|
+
"""Return (data_id, domain, df_with_timeindex) or None for non-timeseries."""
|
|
72
|
+
if isinstance(result, RHEEDVideoResult):
|
|
73
|
+
domain = "rheed"
|
|
74
|
+
df = result.timeseries_data
|
|
75
|
+
elif isinstance(result, OpticalResult):
|
|
76
|
+
domain = "optical"
|
|
77
|
+
df = result.timeseries_data
|
|
78
|
+
elif isinstance(result, MetrologyResult):
|
|
79
|
+
domain = "metrology"
|
|
80
|
+
df = result.timeseries_data
|
|
81
|
+
else:
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
if df is None or df.empty:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
# Build time index: prefer absolute epochs; fall back to upload_datetime + relative offsets.
|
|
88
|
+
upload_dt = getattr(result, "upload_datetime", None)
|
|
89
|
+
|
|
90
|
+
time_index = _infer_absolute_time(df)
|
|
91
|
+
if time_index is None and upload_dt is not None:
|
|
92
|
+
base = pd.to_datetime(upload_dt, utc=True, errors="coerce")
|
|
93
|
+
rel = _infer_relative_time(df)
|
|
94
|
+
if base is not pd.NaT and rel is not None:
|
|
95
|
+
time_index = base + rel
|
|
96
|
+
|
|
97
|
+
if time_index is None:
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
valid_mask = time_index.notna()
|
|
101
|
+
if not valid_mask.any():
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
indexed = df.loc[valid_mask].copy(deep=False)
|
|
105
|
+
indexed.index = pd.Index(time_index[valid_mask], name="time")
|
|
106
|
+
indexed = indexed.sort_index()
|
|
107
|
+
|
|
108
|
+
if not indexed.index.is_unique:
|
|
109
|
+
indexed = indexed[~indexed.index.duplicated(keep="first")]
|
|
110
|
+
|
|
111
|
+
return str(result.data_id), domain, indexed
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _infer_resample_freq(indices: list[pd.DatetimeIndex]) -> pd.Timedelta | None:
|
|
115
|
+
"""Infer a reasonable base frequency from multiple datetime indices."""
|
|
116
|
+
deltas: list[pd.Timedelta] = []
|
|
117
|
+
for idx in indices:
|
|
118
|
+
if idx.size < 2:
|
|
119
|
+
continue
|
|
120
|
+
# use numpy diff to avoid Series construction overhead
|
|
121
|
+
diffs = idx.view("int64")[1:] - idx.view("int64")[:-1]
|
|
122
|
+
if diffs.size:
|
|
123
|
+
median_ns = pd.Series(diffs).median() # nan-safe median
|
|
124
|
+
if pd.notna(median_ns) and median_ns > 0:
|
|
125
|
+
deltas.append(pd.Timedelta(median_ns, unit="ns"))
|
|
126
|
+
|
|
127
|
+
if not deltas:
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
# Use the median of medians to avoid over-densifying to the smallest step
|
|
131
|
+
median_delta = pd.Series(deltas).median()
|
|
132
|
+
if median_delta <= pd.Timedelta(0):
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
# Enforce a floor to avoid overly dense grids
|
|
136
|
+
floor = pd.Timedelta(seconds=0.5)
|
|
137
|
+
if median_delta < floor:
|
|
138
|
+
median_delta = floor
|
|
139
|
+
|
|
140
|
+
# Clamp frequency to keep total grid size reasonable (performance guard)
|
|
141
|
+
min_ts = min(idx[0] for idx in indices if len(idx))
|
|
142
|
+
max_ts = max(idx[-1] for idx in indices if len(idx))
|
|
143
|
+
span = max_ts - min_ts
|
|
144
|
+
if span <= pd.Timedelta(0):
|
|
145
|
+
return median_delta
|
|
146
|
+
|
|
147
|
+
est_points = span / median_delta
|
|
148
|
+
max_points = 1_000_000 # tighter cap for performance
|
|
149
|
+
if est_points > max_points:
|
|
150
|
+
median_delta = span / max_points
|
|
151
|
+
|
|
152
|
+
return median_delta
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def align_timeseries(
|
|
156
|
+
results: Iterable,
|
|
157
|
+
*,
|
|
158
|
+
how: str = "outer",
|
|
159
|
+
) -> pd.DataFrame | None:
|
|
160
|
+
"""Align timeseries results by time index.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
results: Iterable of result objects (RHEEDVideoResult, OpticalResult, MetrologyResult).
|
|
164
|
+
how: Join strategy for the outer alignment. Defaults to "outer".
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
DataFrame | None: Aligned DataFrame with MultiIndex columns (data_id, domain, metric).
|
|
168
|
+
"""
|
|
169
|
+
frames: list[pd.DataFrame] = []
|
|
170
|
+
indices: list[pd.DatetimeIndex] = []
|
|
171
|
+
for item in results:
|
|
172
|
+
extracted = _extract_timeseries(item)
|
|
173
|
+
if not extracted:
|
|
174
|
+
continue
|
|
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)
|
|
181
|
+
|
|
182
|
+
if not frames:
|
|
183
|
+
return pd.DataFrame()
|
|
184
|
+
|
|
185
|
+
join_how = how if how in {"outer", "inner"} else "outer"
|
|
186
|
+
aligned = pd.concat(frames, axis=1, join=join_how, copy=False, sort=False)
|
|
187
|
+
aligned = aligned.sort_index()
|
|
188
|
+
|
|
189
|
+
if how == "left":
|
|
190
|
+
aligned = aligned.reindex(frames[0].index)
|
|
191
|
+
elif how == "right":
|
|
192
|
+
aligned = aligned.reindex(frames[-1].index)
|
|
193
|
+
|
|
194
|
+
freq = _infer_resample_freq(indices)
|
|
195
|
+
if freq:
|
|
196
|
+
aligned = aligned.resample(freq).mean(numeric_only=True)
|
|
197
|
+
|
|
198
|
+
# Drop raw time columns now that the index is the aligned time base.
|
|
199
|
+
if isinstance(aligned.columns, pd.MultiIndex):
|
|
200
|
+
time_metrics = {
|
|
201
|
+
"Time",
|
|
202
|
+
"UNIX Timestamp",
|
|
203
|
+
"Relative Time",
|
|
204
|
+
"timestamp",
|
|
205
|
+
"timestamp_ms",
|
|
206
|
+
"timestamp_seconds",
|
|
207
|
+
}
|
|
208
|
+
aligned = aligned.loc[
|
|
209
|
+
:, [c for c in aligned.columns if c[-1] not in time_metrics]
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
# Merge compatible metrics across items: if multiple columns share (domain, metric)
|
|
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)
|
|
219
|
+
new_cols: dict = {}
|
|
220
|
+
drop_cols: list = []
|
|
221
|
+
|
|
222
|
+
for domain in domains.unique():
|
|
223
|
+
for metric in metrics.unique():
|
|
224
|
+
cols = [
|
|
225
|
+
c
|
|
226
|
+
for c in df.columns
|
|
227
|
+
if c[1] == domain and c[2] == metric and c[0] != "shared"
|
|
228
|
+
]
|
|
229
|
+
if len(cols) <= 1:
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
merged = df[cols[0]]
|
|
233
|
+
conflict = False
|
|
234
|
+
for c in cols[1:]:
|
|
235
|
+
other = df[c]
|
|
236
|
+
overlap_mask = merged.notna() & other.notna()
|
|
237
|
+
if (merged[overlap_mask] != other[overlap_mask]).any():
|
|
238
|
+
conflict = True
|
|
239
|
+
break
|
|
240
|
+
merged = merged.combine_first(other)
|
|
241
|
+
|
|
242
|
+
if conflict:
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
new_col = ("shared", domain, metric)
|
|
246
|
+
new_cols[new_col] = merged
|
|
247
|
+
drop_cols.extend(cols)
|
|
248
|
+
|
|
249
|
+
if new_cols:
|
|
250
|
+
df = df.drop(columns=drop_cols)
|
|
251
|
+
for col, series in new_cols.items():
|
|
252
|
+
df[col] = series
|
|
253
|
+
df = df.sort_index(axis=1)
|
|
254
|
+
return df
|
|
255
|
+
|
|
256
|
+
return _merge_compatible_metrics(aligned)
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from collections.abc import Iterable
|
|
4
|
-
from functools import reduce
|
|
5
|
-
|
|
6
|
-
import pandas as pd
|
|
7
|
-
|
|
8
|
-
from atomscale.results import MetrologyResult, OpticalResult, RHEEDVideoResult
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def _infer_time_column(df: pd.DataFrame) -> pd.Series:
|
|
12
|
-
"""Return a datetime-like series suitable for alignment."""
|
|
13
|
-
candidates = [
|
|
14
|
-
"UNIX Timestamp",
|
|
15
|
-
"unix_timestamp_ms",
|
|
16
|
-
"timestamp_ms",
|
|
17
|
-
"timestamp",
|
|
18
|
-
"time_seconds",
|
|
19
|
-
"relative_time_seconds",
|
|
20
|
-
"Time",
|
|
21
|
-
]
|
|
22
|
-
for col in candidates:
|
|
23
|
-
if col in df.columns:
|
|
24
|
-
series = df[col]
|
|
25
|
-
# Heuristically pick unit for integer epoch values.
|
|
26
|
-
if pd.api.types.is_integer_dtype(series):
|
|
27
|
-
max_val = series.max(skipna=True)
|
|
28
|
-
if max_val > 1e12:
|
|
29
|
-
dt = pd.to_datetime(series, unit="ns", errors="coerce")
|
|
30
|
-
elif max_val > 1e10:
|
|
31
|
-
dt = pd.to_datetime(series, unit="ms", errors="coerce")
|
|
32
|
-
else:
|
|
33
|
-
dt = pd.to_datetime(series, unit="s", errors="coerce")
|
|
34
|
-
elif pd.api.types.is_float_dtype(series):
|
|
35
|
-
dt = pd.to_datetime(series, unit="s", errors="coerce")
|
|
36
|
-
else:
|
|
37
|
-
dt = pd.to_datetime(series, errors="coerce")
|
|
38
|
-
return dt
|
|
39
|
-
# Fallback to existing index if no candidate column is present.
|
|
40
|
-
if df.index.nlevels == 1:
|
|
41
|
-
idx = df.index
|
|
42
|
-
if pd.api.types.is_datetime64_any_dtype(idx):
|
|
43
|
-
return pd.Series(idx, index=df.index)
|
|
44
|
-
# Final fallback: monotonically increasing integers.
|
|
45
|
-
return pd.Series(range(len(df)), index=df.index).astype("int64")
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def _extract_timeseries(result):
|
|
49
|
-
"""Return (data_id, domain, df_with_timeindex) or None for non-timeseries."""
|
|
50
|
-
if isinstance(result, RHEEDVideoResult):
|
|
51
|
-
domain = "rheed"
|
|
52
|
-
df = result.timeseries_data
|
|
53
|
-
elif isinstance(result, OpticalResult):
|
|
54
|
-
domain = "optical"
|
|
55
|
-
df = result.timeseries_data
|
|
56
|
-
elif isinstance(result, MetrologyResult):
|
|
57
|
-
domain = "metrology"
|
|
58
|
-
df = result.timeseries_data
|
|
59
|
-
else:
|
|
60
|
-
return None
|
|
61
|
-
|
|
62
|
-
if df is None or df.empty:
|
|
63
|
-
return None
|
|
64
|
-
|
|
65
|
-
flat_df = df.copy().reset_index()
|
|
66
|
-
flat_df["__time__"] = _infer_time_column(flat_df)
|
|
67
|
-
flat_df = flat_df.set_index("__time__")
|
|
68
|
-
flat_df.index.name = "time"
|
|
69
|
-
flat_df = flat_df.sort_index()
|
|
70
|
-
|
|
71
|
-
return str(result.data_id), domain, flat_df
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def align_timeseries(
|
|
75
|
-
results: Iterable,
|
|
76
|
-
*,
|
|
77
|
-
how: str = "outer",
|
|
78
|
-
resample: str | None = None,
|
|
79
|
-
) -> pd.DataFrame | None:
|
|
80
|
-
"""Align timeseries results by time index.
|
|
81
|
-
|
|
82
|
-
Args:
|
|
83
|
-
results: Iterable of result objects (RHEEDVideoResult, OpticalResult, MetrologyResult).
|
|
84
|
-
how: Join strategy for the outer alignment. Defaults to "outer".
|
|
85
|
-
resample: Optional pandas resample rule (e.g., "1s").
|
|
86
|
-
|
|
87
|
-
Returns:
|
|
88
|
-
DataFrame | None: Aligned DataFrame with MultiIndex columns (data_id, domain, metric).
|
|
89
|
-
"""
|
|
90
|
-
frames = []
|
|
91
|
-
for item in results:
|
|
92
|
-
extracted = _extract_timeseries(item)
|
|
93
|
-
if not extracted:
|
|
94
|
-
continue
|
|
95
|
-
data_id, domain, df = extracted
|
|
96
|
-
df = df.copy()
|
|
97
|
-
df.columns = pd.MultiIndex.from_product([[data_id], [domain], df.columns])
|
|
98
|
-
frames.append(df)
|
|
99
|
-
|
|
100
|
-
if not frames:
|
|
101
|
-
return None
|
|
102
|
-
|
|
103
|
-
aligned = reduce(lambda a, b: a.join(b, how=how), frames)
|
|
104
|
-
if resample:
|
|
105
|
-
aligned = aligned.resample(resample).mean()
|
|
106
|
-
return aligned
|
|
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
|
|
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
|
|
File without changes
|
{atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-macos-latest_py3.10_extras.txt
RENAMED
|
File without changes
|
|
File without changes
|
{atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-macos-latest_py3.11_extras.txt
RENAMED
|
File without changes
|
|
File without changes
|
{atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-macos-latest_py3.12_extras.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-ubuntu-latest_py3.10_extras.txt
RENAMED
|
File without changes
|
|
File without changes
|
{atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-ubuntu-latest_py3.11_extras.txt
RENAMED
|
File without changes
|
|
File without changes
|
{atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-ubuntu-latest_py3.12_extras.txt
RENAMED
|
File without changes
|
|
File without changes
|
{atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-ubuntu-latest_py3.9_extras.txt
RENAMED
|
File without changes
|
|
File without changes
|
{atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-windows-latest_py3.10_extras.txt
RENAMED
|
File without changes
|
|
File without changes
|
{atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-windows-latest_py3.11_extras.txt
RENAMED
|
File without changes
|
|
File without changes
|
{atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-windows-latest_py3.12_extras.txt
RENAMED
|
File without changes
|
|
File without changes
|
{atomscale-0.7.0 → atomscale-0.7.2}/requirements/requirements-windows-latest_py3.9_extras.txt
RENAMED
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|