mfethuls 0.3.1__py3-none-any.whl

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 (151) hide show
  1. mfethuls/__init__.py +26 -0
  2. mfethuls/_files.py +22 -0
  3. mfethuls/app/__init__.py +6 -0
  4. mfethuls/app/backend.py +186 -0
  5. mfethuls/app/launch.py +21 -0
  6. mfethuls/app/page.py +775 -0
  7. mfethuls/app/register.py +292 -0
  8. mfethuls/app/streamlit_app.py +6 -0
  9. mfethuls/app/welcome.py +526 -0
  10. mfethuls/characterizers/__init__.py +0 -0
  11. mfethuls/characterizers/dsc.py +84 -0
  12. mfethuls/characterizers/tga.py +29 -0
  13. mfethuls/comparison.py +171 -0
  14. mfethuls/config/__init__.py +0 -0
  15. mfethuls/config/instrument_params.json +126 -0
  16. mfethuls/config/loader.py +313 -0
  17. mfethuls/config/schemas/dma.json +91 -0
  18. mfethuls/config/schemas/dsc.json +97 -0
  19. mfethuls/config/schemas/fluorescence.json +27 -0
  20. mfethuls/config/schemas/ftir.json +32 -0
  21. mfethuls/config/schemas/ms.json +27 -0
  22. mfethuls/config/schemas/nmr.json +27 -0
  23. mfethuls/config/schemas/rheometer.json +91 -0
  24. mfethuls/config/schemas/saxs.json +37 -0
  25. mfethuls/config/schemas/sec.json +36 -0
  26. mfethuls/config/schemas/tga.json +60 -0
  27. mfethuls/config/schemas/uv_vis.json +47 -0
  28. mfethuls/dataset.py +49 -0
  29. mfethuls/discovery.py +415 -0
  30. mfethuls/examples/__init__.py +102 -0
  31. mfethuls/examples/data/DSC/poly1.txt +6034 -0
  32. mfethuls/examples/data/DSC/poly2.txt +8194 -0
  33. mfethuls/examples/data/FTIR/poly1.csv +3403 -0
  34. mfethuls/examples/data/FTIR/poly2.csv +3403 -0
  35. mfethuls/examples/data/Fluorescence/poly3/poly3__0__18-11-09-757.txt +1058 -0
  36. mfethuls/examples/data/Fluorescence/poly3/poly3__10__18-11-19-757.txt +1058 -0
  37. mfethuls/examples/data/Fluorescence/poly3/poly3__11__18-11-20-757.txt +1058 -0
  38. mfethuls/examples/data/Fluorescence/poly3/poly3__12__18-11-21-757.txt +1058 -0
  39. mfethuls/examples/data/Fluorescence/poly3/poly3__13__18-11-22-757.txt +1058 -0
  40. mfethuls/examples/data/Fluorescence/poly3/poly3__14__18-11-23-757.txt +1058 -0
  41. mfethuls/examples/data/Fluorescence/poly3/poly3__15__18-11-24-757.txt +1058 -0
  42. mfethuls/examples/data/Fluorescence/poly3/poly3__16__18-11-25-757.txt +1058 -0
  43. mfethuls/examples/data/Fluorescence/poly3/poly3__17__18-11-26-757.txt +1058 -0
  44. mfethuls/examples/data/Fluorescence/poly3/poly3__18__18-11-27-757.txt +1058 -0
  45. mfethuls/examples/data/Fluorescence/poly3/poly3__19__18-11-28-758.txt +1058 -0
  46. mfethuls/examples/data/Fluorescence/poly3/poly3__1__18-11-10-757.txt +1058 -0
  47. mfethuls/examples/data/Fluorescence/poly3/poly3__20__18-11-29-757.txt +1058 -0
  48. mfethuls/examples/data/Fluorescence/poly3/poly3__21__18-11-30-757.txt +1058 -0
  49. mfethuls/examples/data/Fluorescence/poly3/poly3__22__18-11-31-744.txt +1058 -0
  50. mfethuls/examples/data/Fluorescence/poly3/poly3__23__18-11-32-758.txt +1058 -0
  51. mfethuls/examples/data/Fluorescence/poly3/poly3__24__18-11-33-758.txt +1058 -0
  52. mfethuls/examples/data/Fluorescence/poly3/poly3__25__18-11-34-758.txt +1058 -0
  53. mfethuls/examples/data/Fluorescence/poly3/poly3__26__18-11-35-758.txt +1058 -0
  54. mfethuls/examples/data/Fluorescence/poly3/poly3__27__18-11-36-758.txt +1058 -0
  55. mfethuls/examples/data/Fluorescence/poly3/poly3__28__18-11-37-758.txt +1058 -0
  56. mfethuls/examples/data/Fluorescence/poly3/poly3__29__18-11-38-758.txt +1058 -0
  57. mfethuls/examples/data/Fluorescence/poly3/poly3__2__18-11-11-757.txt +1058 -0
  58. mfethuls/examples/data/Fluorescence/poly3/poly3__30__18-11-39-758.txt +1058 -0
  59. mfethuls/examples/data/Fluorescence/poly3/poly3__31__18-11-40-758.txt +1058 -0
  60. mfethuls/examples/data/Fluorescence/poly3/poly3__32__18-11-41-758.txt +1058 -0
  61. mfethuls/examples/data/Fluorescence/poly3/poly3__33__18-11-42-758.txt +1058 -0
  62. mfethuls/examples/data/Fluorescence/poly3/poly3__34__18-11-43-758.txt +1058 -0
  63. mfethuls/examples/data/Fluorescence/poly3/poly3__35__18-11-44-758.txt +1058 -0
  64. mfethuls/examples/data/Fluorescence/poly3/poly3__36__18-11-45-757.txt +1058 -0
  65. mfethuls/examples/data/Fluorescence/poly3/poly3__37__18-11-46-758.txt +1058 -0
  66. mfethuls/examples/data/Fluorescence/poly3/poly3__38__18-11-47-758.txt +1058 -0
  67. mfethuls/examples/data/Fluorescence/poly3/poly3__39__18-11-48-758.txt +1058 -0
  68. mfethuls/examples/data/Fluorescence/poly3/poly3__3__18-11-12-757.txt +1058 -0
  69. mfethuls/examples/data/Fluorescence/poly3/poly3__40__18-11-49-758.txt +1058 -0
  70. mfethuls/examples/data/Fluorescence/poly3/poly3__41__18-11-50-758.txt +1058 -0
  71. mfethuls/examples/data/Fluorescence/poly3/poly3__42__18-11-51-758.txt +1058 -0
  72. mfethuls/examples/data/Fluorescence/poly3/poly3__43__18-11-52-758.txt +1058 -0
  73. mfethuls/examples/data/Fluorescence/poly3/poly3__44__18-11-53-758.txt +1058 -0
  74. mfethuls/examples/data/Fluorescence/poly3/poly3__45__18-11-54-758.txt +1058 -0
  75. mfethuls/examples/data/Fluorescence/poly3/poly3__46__18-11-55-758.txt +1058 -0
  76. mfethuls/examples/data/Fluorescence/poly3/poly3__47__18-11-56-758.txt +1058 -0
  77. mfethuls/examples/data/Fluorescence/poly3/poly3__48__18-11-57-758.txt +1058 -0
  78. mfethuls/examples/data/Fluorescence/poly3/poly3__49__18-11-58-758.txt +1058 -0
  79. mfethuls/examples/data/Fluorescence/poly3/poly3__4__18-11-13-757.txt +1058 -0
  80. mfethuls/examples/data/Fluorescence/poly3/poly3__5__18-11-14-757.txt +1058 -0
  81. mfethuls/examples/data/Fluorescence/poly3/poly3__6__18-11-15-757.txt +1058 -0
  82. mfethuls/examples/data/Fluorescence/poly3/poly3__7__18-11-16-757.txt +1058 -0
  83. mfethuls/examples/data/Fluorescence/poly3/poly3__8__18-11-17-757.txt +1058 -0
  84. mfethuls/examples/data/Fluorescence/poly3/poly3__9__18-11-18-757.txt +1058 -0
  85. mfethuls/examples/data/TGA/poly1.txt +2854 -0
  86. mfethuls/examples/data/TGA/poly2.txt +2854 -0
  87. mfethuls/examples/experiments_registry.csv +8 -0
  88. mfethuls/experiments.py +427 -0
  89. mfethuls/factory.py +134 -0
  90. mfethuls/instruments/__init__.py +0 -0
  91. mfethuls/instruments/base.py +6 -0
  92. mfethuls/instruments/generic.py +17 -0
  93. mfethuls/main.py +154 -0
  94. mfethuls/manifest.py +325 -0
  95. mfethuls/parsers/__init__.py +3 -0
  96. mfethuls/parsers/dma.py +127 -0
  97. mfethuls/parsers/dsc.py +439 -0
  98. mfethuls/parsers/ftir.py +81 -0
  99. mfethuls/parsers/ingestion.py +80 -0
  100. mfethuls/parsers/ms.py +116 -0
  101. mfethuls/parsers/nmr.py +141 -0
  102. mfethuls/parsers/registry.py +35 -0
  103. mfethuls/parsers/rheometer.py +113 -0
  104. mfethuls/parsers/saxs.py +85 -0
  105. mfethuls/parsers/sec.py +110 -0
  106. mfethuls/parsers/tga.py +136 -0
  107. mfethuls/parsers/uv_vis.py +189 -0
  108. mfethuls/plotting/__init__.py +46 -0
  109. mfethuls/plotting/backend.py +51 -0
  110. mfethuls/plotting/comparison.py +296 -0
  111. mfethuls/plotting/core.py +398 -0
  112. mfethuls/plotting/dma.py +97 -0
  113. mfethuls/plotting/dsc.py +180 -0
  114. mfethuls/plotting/fluorescence.py +56 -0
  115. mfethuls/plotting/ftir.py +65 -0
  116. mfethuls/plotting/labels.py +148 -0
  117. mfethuls/plotting/ms.py +53 -0
  118. mfethuls/plotting/nmr.py +55 -0
  119. mfethuls/plotting/render_mpl.py +193 -0
  120. mfethuls/plotting/render_plotly.py +212 -0
  121. mfethuls/plotting/rheology.py +97 -0
  122. mfethuls/plotting/saxs.py +55 -0
  123. mfethuls/plotting/sec.py +93 -0
  124. mfethuls/plotting/spec.py +118 -0
  125. mfethuls/plotting/style.py +67 -0
  126. mfethuls/plotting/tga.py +60 -0
  127. mfethuls/plotting/uv_vis.py +63 -0
  128. mfethuls/py.typed +0 -0
  129. mfethuls/reconcile.py +419 -0
  130. mfethuls/registry_validator.py +481 -0
  131. mfethuls/schema_normalization.py +207 -0
  132. mfethuls/settings.py +204 -0
  133. mfethuls/storage/__init__.py +23 -0
  134. mfethuls/storage/backends.py +105 -0
  135. mfethuls/storage/catalog.py +82 -0
  136. mfethuls/storage/config.py +67 -0
  137. mfethuls/storage/duckdb_backend.py +293 -0
  138. mfethuls/storage/manager.py +102 -0
  139. mfethuls/storage/notebook.py +26 -0
  140. mfethuls/storage/provenance.py +88 -0
  141. mfethuls/storage/types.py +83 -0
  142. mfethuls/stores.py +50 -0
  143. mfethuls/tutorials/__init__.py +53 -0
  144. mfethuls/tutorials/local_tutorial.py +345 -0
  145. mfethuls-0.3.1.dist-info/METADATA +166 -0
  146. mfethuls-0.3.1.dist-info/RECORD +151 -0
  147. mfethuls-0.3.1.dist-info/WHEEL +5 -0
  148. mfethuls-0.3.1.dist-info/entry_points.txt +4 -0
  149. mfethuls-0.3.1.dist-info/licenses/LICENSE +202 -0
  150. mfethuls-0.3.1.dist-info/licenses/NOTICE +5 -0
  151. mfethuls-0.3.1.dist-info/top_level.txt +1 -0
mfethuls/__init__.py ADDED
@@ -0,0 +1,26 @@
1
+ from .comparison import ComparisonSet, load_experiments, load_samples
2
+ from .settings import use_test_env
3
+ from .experiments import Registry
4
+
5
+
6
+ def plot_experiments(*args, **kwargs):
7
+ from .plotting.comparison import plot_experiments as _plot_experiments
8
+
9
+ return _plot_experiments(*args, **kwargs)
10
+
11
+
12
+ def set_plot_backend(backend):
13
+ """Default plotting backend: "matplotlib" (publication, vector SVG) or "plotly" (interactive)."""
14
+ from .plotting.backend import set_default_backend
15
+
16
+ set_default_backend(backend)
17
+
18
+ __all__ = [
19
+ "ComparisonSet",
20
+ "Registry",
21
+ "load_experiments",
22
+ "load_samples",
23
+ "plot_experiments",
24
+ "set_plot_backend",
25
+ "use_test_env",
26
+ ]
mfethuls/_files.py ADDED
@@ -0,0 +1,22 @@
1
+ """Small file helpers shared by the manifest and the saved settings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import time
7
+
8
+
9
+ def replace_with_retry(source: str, target: str, attempts: int = 10) -> None:
10
+ """``os.replace``, retried briefly: on Windows it fails while another process has the target open.
11
+
12
+ Virus scanners, indexers and sync clients (OneDrive) open freshly written files for
13
+ a moment; waiting a little and trying again is enough.
14
+ """
15
+ for attempt in range(attempts):
16
+ try:
17
+ os.replace(source, target)
18
+ return
19
+ except PermissionError:
20
+ if attempt == attempts - 1:
21
+ raise
22
+ time.sleep(0.05 * (attempt + 1))
@@ -0,0 +1,6 @@
1
+ """The mfethuls Streamlit app: ``mfethuls-app`` starts it on this computer.
2
+
3
+ - :mod:`mfethuls.app.page`: the page, drawn by ``run(backend)``
4
+ - :mod:`mfethuls.app.backend`: where its data comes from (:class:`LocalBackend`)
5
+ - :mod:`mfethuls.app.register`: the "Register new data" dialog
6
+ """
@@ -0,0 +1,186 @@
1
+ """Where the app gets its data: the backend :func:`mfethuls.app.page.run` draws the page for.
2
+
3
+ :class:`LocalBackend` works on this computer: the registry file, the data folder and
4
+ the local stores (manifest, Parquet files, DuckDB catalog). Other backends subclass
5
+ it and override what differs, e.g. reading datasets through an API.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from typing import Any, Dict, List, Optional
12
+
13
+ import pandas as pd
14
+ import streamlit as st
15
+
16
+ from mfethuls.app.register import clear_registry_caches, new_data_prompt, registry_frame
17
+ from mfethuls.app.welcome import is_set_up
18
+ from mfethuls.settings import get_settings
19
+
20
+ _CATALOG_TEXT_FIELDS = (
21
+ "experiment_id", "experiment_name", "raw_data_filename", "instrument_name",
22
+ "instrument_type", "instrument_model", "sample_id", "run_id", "measurement_profile",
23
+ )
24
+
25
+
26
+ def normalise_dataset(row: Dict[str, Any]) -> Dict[str, Any]:
27
+ """A catalog row with empty strings for missing text, and its view name as ``name``."""
28
+ dataset = {
29
+ "name": row.get("name") or row.get("table_name") or "",
30
+ "storage_path": row.get("storage_path") or "",
31
+ "registered_at": str(row.get("registered_at") or ""),
32
+ "rows": row.get("rows"),
33
+ }
34
+ dataset.update({field: row.get(field) or "" for field in _CATALOG_TEXT_FIELDS})
35
+ return dataset
36
+
37
+
38
+ def local_catalog_path() -> Optional[str]:
39
+ """The DuckDB catalog file, if it exists on this computer."""
40
+ from mfethuls.storage import _get_duckdb_path
41
+
42
+ path = _get_duckdb_path()
43
+ return path if os.path.exists(path) else None
44
+
45
+
46
+ def list_local_datasets() -> List[Dict[str, Any]]:
47
+ """Ingested datasets from the local DuckDB catalog (cached for 30 s, per catalog)."""
48
+ db_path = local_catalog_path()
49
+ return _list_catalog(db_path) if db_path else []
50
+
51
+
52
+ @st.cache_data(show_spinner=False, ttl=30)
53
+ def _list_catalog(db_path: str) -> List[Dict[str, Any]]:
54
+ from mfethuls.storage import duckdb_session
55
+
56
+ with duckdb_session(db_path=db_path, read_only=True) as catalog:
57
+ return [normalise_dataset(row) for row in catalog.list_registered()]
58
+
59
+
60
+ # Cached for 10 minutes: whole datasets are expensive to reload. Refresh clears it.
61
+ def query_local_dataset(name: str, limit: Optional[int] = None, offset: int = 0) -> pd.DataFrame:
62
+ """Rows of a dataset from the local catalog; ``limit=None`` loads all of them."""
63
+ return _query_catalog(local_catalog_path(), name, limit, offset)
64
+
65
+
66
+ @st.cache_data(show_spinner=False, ttl=600)
67
+ def _query_catalog(db_path: str, name: str, limit: Optional[int], offset: int) -> pd.DataFrame:
68
+ from mfethuls.storage import duckdb_session
69
+
70
+ with duckdb_session(db_path=db_path, read_only=True) as catalog:
71
+ safe = name.replace('"', '""')
72
+ if limit is None:
73
+ return catalog.query(f'SELECT * FROM "{safe}" OFFSET ?', [offset])
74
+ return catalog.query(f'SELECT * FROM "{safe}" LIMIT ? OFFSET ?', [limit, offset])
75
+
76
+
77
+ class LocalBackend:
78
+ """The app on this computer: it reads and writes the registry file and the local stores."""
79
+
80
+ # The "Register new data" dialog may append rows to the registry file.
81
+ writes_registry = True
82
+ # The getting-started page offers to switch to a copy of the bundled examples.
83
+ offers_example = True
84
+
85
+ # -- sidebar ------------------------------------------------------------
86
+
87
+ def render_status(self) -> None:
88
+ """Anything shown at the top of the sidebar (nothing locally)."""
89
+
90
+ def render_ingest(self) -> str:
91
+ """The Ingest sidebar section; returns the registry path in use."""
92
+ registry_path = get_settings().registry_path or ""
93
+ if not is_set_up():
94
+ st.caption("Choose your data folder and registry first: **⚙ Settings** → Data folder and registry.")
95
+ return registry_path
96
+ st.caption(f"Registry: `{os.path.basename(registry_path)}`")
97
+ refresh_ingest = st.checkbox("Re-parse even if cached", value=False)
98
+
99
+ experiment_names: List[str] = []
100
+ if registry_path:
101
+ new_data_prompt(self, registry_path)
102
+ try:
103
+ experiment_names = registry_frame(registry_path).get("name", pd.Series(dtype=str)).dropna().astype(str).tolist()
104
+ except Exception as exc:
105
+ st.error(f"Could not load registry: {exc}")
106
+
107
+ selected: List[str] = []
108
+ if experiment_names:
109
+ select_all = st.checkbox("Select all", value=False)
110
+ selected = experiment_names if select_all else st.multiselect("Experiments", options=experiment_names)
111
+ st.caption(f"{len(selected)} selected.")
112
+ elif registry_path:
113
+ st.info("No experiments found.")
114
+
115
+ if st.button("Ingest", disabled=not selected, width="stretch"):
116
+ progress_bar = st.progress(0)
117
+ results: List[tuple] = []
118
+ for i, name in enumerate(selected):
119
+ results.extend(self.ingest([name], registry_path, refresh=refresh_ingest))
120
+ progress_bar.progress((i + 1) / len(selected))
121
+
122
+ counts: Dict[str, int] = {}
123
+ for name, result in results:
124
+ status = result.get("status", "unknown")
125
+ counts[status] = counts.get(status, 0) + 1
126
+ if status == "error":
127
+ st.error(f"{name}: {result.get('error', '')}")
128
+ st.success("Done — " + ", ".join(f"{k}: {v}" for k, v in counts.items()))
129
+ self.clear_caches()
130
+ st.rerun()
131
+ return registry_path
132
+
133
+ # -- data -----------------------------------------------------------------
134
+
135
+ def list_datasets(self) -> List[Dict[str, Any]]:
136
+ return list_local_datasets()
137
+
138
+ def query_dataset(self, name: str, limit: Optional[int] = None, offset: int = 0) -> pd.DataFrame:
139
+ return query_local_dataset(name, limit, offset)
140
+
141
+ def clear_caches(self) -> None:
142
+ _list_catalog.clear()
143
+ _query_catalog.clear()
144
+
145
+ def ingest(self, names: List[str], registry_path: str, refresh: bool = False) -> List[tuple]:
146
+ """Ingest experiments into the local stores; returns ``(name, result)`` pairs."""
147
+ from mfethuls.config.loader import ingest_experiment_dataset
148
+ from mfethuls.experiments import Registry
149
+ from mfethuls.stores import open_local_stores
150
+
151
+ registry = Registry.load(registry_path)
152
+ results = []
153
+ with open_local_stores() as stores:
154
+ for name in names:
155
+ try:
156
+ result = ingest_experiment_dataset(name, refresh=refresh, registry=registry, stores=stores)
157
+ results.append((name, result or {}))
158
+ except Exception as exc:
159
+ results.append((name, {"status": "error", "error": str(exc)}))
160
+ _list_catalog.clear()
161
+ return results
162
+
163
+ # -- clean up ---------------------------------------------------------------
164
+
165
+ def reconcile_plan(self, registry_path: str) -> Dict[str, Any]:
166
+ from mfethuls.experiments import Registry
167
+ from mfethuls.reconcile import plan
168
+ from mfethuls.stores import open_local_stores
169
+
170
+ with open_local_stores() as stores:
171
+ return plan(Registry.load(registry_path), stores).to_dict()
172
+
173
+ def reconcile_apply(self, registry_path: str, fingerprint: str, force: bool = False) -> List[Dict[str, Any]]:
174
+ """Apply the reviewed plan; RuntimeError if it changed since the check or is blocked."""
175
+ from mfethuls.experiments import Registry
176
+ from mfethuls.reconcile import PlanChanged, ReconcileBlocked, apply_reviewed
177
+ from mfethuls.stores import open_local_stores
178
+
179
+ with open_local_stores() as stores:
180
+ try:
181
+ results = apply_reviewed(Registry.load(registry_path), stores, fingerprint=fingerprint, force=force)
182
+ except (PlanChanged, ReconcileBlocked) as exc:
183
+ raise RuntimeError(str(exc)) from exc
184
+ clear_registry_caches()
185
+ self.clear_caches()
186
+ return results
mfethuls/app/launch.py ADDED
@@ -0,0 +1,21 @@
1
+ """``mfethuls-app``: start the Streamlit app. Extra arguments are passed to ``streamlit run``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+
9
+
10
+ def run_streamlit(script: Path, argv: list[str] | None = None) -> int:
11
+ """Run ``streamlit run script`` with this Python; returns its exit code."""
12
+ args = sys.argv[1:] if argv is None else argv
13
+ return subprocess.call([sys.executable, "-m", "streamlit", "run", str(script), *args])
14
+
15
+
16
+ def main(argv: list[str] | None = None) -> int:
17
+ return run_streamlit(Path(__file__).with_name("streamlit_app.py"), argv)
18
+
19
+
20
+ if __name__ == "__main__":
21
+ raise SystemExit(main())