sigima 0.0.1.dev0__py3-none-any.whl → 1.0.0__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 (264) hide show
  1. sigima/__init__.py +142 -2
  2. sigima/client/__init__.py +105 -0
  3. sigima/client/base.py +780 -0
  4. sigima/client/remote.py +469 -0
  5. sigima/client/stub.py +814 -0
  6. sigima/client/utils.py +90 -0
  7. sigima/config.py +444 -0
  8. sigima/data/logo/Sigima.svg +135 -0
  9. sigima/data/tests/annotations.json +798 -0
  10. sigima/data/tests/curve_fitting/exponential_fit.txt +511 -0
  11. sigima/data/tests/curve_fitting/gaussian_fit.txt +100 -0
  12. sigima/data/tests/curve_fitting/piecewiseexponential_fit.txt +1022 -0
  13. sigima/data/tests/curve_fitting/polynomial_fit.txt +100 -0
  14. sigima/data/tests/curve_fitting/twohalfgaussian_fit.txt +1000 -0
  15. sigima/data/tests/curve_formats/bandwidth.txt +201 -0
  16. sigima/data/tests/curve_formats/boxcar.npy +0 -0
  17. sigima/data/tests/curve_formats/datetime.txt +1001 -0
  18. sigima/data/tests/curve_formats/dynamic_parameters.txt +4000 -0
  19. sigima/data/tests/curve_formats/fw1e2.txt +301 -0
  20. sigima/data/tests/curve_formats/fwhm.txt +319 -0
  21. sigima/data/tests/curve_formats/multiple_curves.csv +29 -0
  22. sigima/data/tests/curve_formats/noised_saw.mat +0 -0
  23. sigima/data/tests/curve_formats/oscilloscope.csv +111 -0
  24. sigima/data/tests/curve_formats/other/other2/recursive2.txt +5 -0
  25. sigima/data/tests/curve_formats/other/recursive1.txt +5 -0
  26. sigima/data/tests/curve_formats/paracetamol.npy +0 -0
  27. sigima/data/tests/curve_formats/paracetamol.txt +1010 -0
  28. sigima/data/tests/curve_formats/paracetamol_dx_dy.csv +1000 -0
  29. sigima/data/tests/curve_formats/paracetamol_dy.csv +1001 -0
  30. sigima/data/tests/curve_formats/pulse1.npy +0 -0
  31. sigima/data/tests/curve_formats/pulse2.npy +0 -0
  32. sigima/data/tests/curve_formats/simple.txt +5 -0
  33. sigima/data/tests/curve_formats/spectrum.mca +2139 -0
  34. sigima/data/tests/curve_formats/square2.npy +0 -0
  35. sigima/data/tests/curve_formats/step.npy +0 -0
  36. sigima/data/tests/fabry-perot1.jpg +0 -0
  37. sigima/data/tests/fabry-perot2.jpg +0 -0
  38. sigima/data/tests/flower.npy +0 -0
  39. sigima/data/tests/image_formats/NF 180338201.scor-data +11003 -0
  40. sigima/data/tests/image_formats/binary_image.npy +0 -0
  41. sigima/data/tests/image_formats/binary_image.png +0 -0
  42. sigima/data/tests/image_formats/centroid_test.npy +0 -0
  43. sigima/data/tests/image_formats/coordinated_text/complex_image.txt +10011 -0
  44. sigima/data/tests/image_formats/coordinated_text/complex_ref_image.txt +10010 -0
  45. sigima/data/tests/image_formats/coordinated_text/image.txt +15 -0
  46. sigima/data/tests/image_formats/coordinated_text/image2.txt +14 -0
  47. sigima/data/tests/image_formats/coordinated_text/image_no_unit_no_label.txt +14 -0
  48. sigima/data/tests/image_formats/coordinated_text/image_with_nan.txt +15 -0
  49. sigima/data/tests/image_formats/coordinated_text/image_with_unit.txt +14 -0
  50. sigima/data/tests/image_formats/fiber.csv +480 -0
  51. sigima/data/tests/image_formats/fiber.jpg +0 -0
  52. sigima/data/tests/image_formats/fiber.png +0 -0
  53. sigima/data/tests/image_formats/fiber.txt +480 -0
  54. sigima/data/tests/image_formats/gaussian_spot_with_noise.npy +0 -0
  55. sigima/data/tests/image_formats/mr-brain.dcm +0 -0
  56. sigima/data/tests/image_formats/noised_gaussian.mat +0 -0
  57. sigima/data/tests/image_formats/sif_reader/nd_lum_image_no_glue.sif +0 -0
  58. sigima/data/tests/image_formats/sif_reader/raman1.sif +0 -0
  59. sigima/data/tests/image_formats/tiling.txt +10 -0
  60. sigima/data/tests/image_formats/uint16.tiff +0 -0
  61. sigima/data/tests/image_formats/uint8.tiff +0 -0
  62. sigima/data/tests/laser_beam/TEM00_z_13.jpg +0 -0
  63. sigima/data/tests/laser_beam/TEM00_z_18.jpg +0 -0
  64. sigima/data/tests/laser_beam/TEM00_z_23.jpg +0 -0
  65. sigima/data/tests/laser_beam/TEM00_z_30.jpg +0 -0
  66. sigima/data/tests/laser_beam/TEM00_z_35.jpg +0 -0
  67. sigima/data/tests/laser_beam/TEM00_z_40.jpg +0 -0
  68. sigima/data/tests/laser_beam/TEM00_z_45.jpg +0 -0
  69. sigima/data/tests/laser_beam/TEM00_z_50.jpg +0 -0
  70. sigima/data/tests/laser_beam/TEM00_z_55.jpg +0 -0
  71. sigima/data/tests/laser_beam/TEM00_z_60.jpg +0 -0
  72. sigima/data/tests/laser_beam/TEM00_z_65.jpg +0 -0
  73. sigima/data/tests/laser_beam/TEM00_z_70.jpg +0 -0
  74. sigima/data/tests/laser_beam/TEM00_z_75.jpg +0 -0
  75. sigima/data/tests/laser_beam/TEM00_z_80.jpg +0 -0
  76. sigima/enums.py +195 -0
  77. sigima/io/__init__.py +123 -0
  78. sigima/io/base.py +311 -0
  79. sigima/io/common/__init__.py +5 -0
  80. sigima/io/common/basename.py +164 -0
  81. sigima/io/common/converters.py +189 -0
  82. sigima/io/common/objmeta.py +181 -0
  83. sigima/io/common/textreader.py +58 -0
  84. sigima/io/convenience.py +157 -0
  85. sigima/io/enums.py +17 -0
  86. sigima/io/ftlab.py +395 -0
  87. sigima/io/image/__init__.py +9 -0
  88. sigima/io/image/base.py +177 -0
  89. sigima/io/image/formats.py +1016 -0
  90. sigima/io/image/funcs.py +414 -0
  91. sigima/io/signal/__init__.py +9 -0
  92. sigima/io/signal/base.py +129 -0
  93. sigima/io/signal/formats.py +290 -0
  94. sigima/io/signal/funcs.py +723 -0
  95. sigima/objects/__init__.py +260 -0
  96. sigima/objects/base.py +937 -0
  97. sigima/objects/image/__init__.py +88 -0
  98. sigima/objects/image/creation.py +556 -0
  99. sigima/objects/image/object.py +524 -0
  100. sigima/objects/image/roi.py +904 -0
  101. sigima/objects/scalar/__init__.py +57 -0
  102. sigima/objects/scalar/common.py +215 -0
  103. sigima/objects/scalar/geometry.py +502 -0
  104. sigima/objects/scalar/table.py +784 -0
  105. sigima/objects/shape.py +290 -0
  106. sigima/objects/signal/__init__.py +133 -0
  107. sigima/objects/signal/constants.py +27 -0
  108. sigima/objects/signal/creation.py +1428 -0
  109. sigima/objects/signal/object.py +444 -0
  110. sigima/objects/signal/roi.py +274 -0
  111. sigima/params.py +405 -0
  112. sigima/proc/__init__.py +96 -0
  113. sigima/proc/base.py +381 -0
  114. sigima/proc/decorator.py +330 -0
  115. sigima/proc/image/__init__.py +513 -0
  116. sigima/proc/image/arithmetic.py +335 -0
  117. sigima/proc/image/base.py +260 -0
  118. sigima/proc/image/detection.py +519 -0
  119. sigima/proc/image/edges.py +329 -0
  120. sigima/proc/image/exposure.py +406 -0
  121. sigima/proc/image/extraction.py +458 -0
  122. sigima/proc/image/filtering.py +219 -0
  123. sigima/proc/image/fourier.py +147 -0
  124. sigima/proc/image/geometry.py +661 -0
  125. sigima/proc/image/mathops.py +340 -0
  126. sigima/proc/image/measurement.py +195 -0
  127. sigima/proc/image/morphology.py +155 -0
  128. sigima/proc/image/noise.py +107 -0
  129. sigima/proc/image/preprocessing.py +182 -0
  130. sigima/proc/image/restoration.py +235 -0
  131. sigima/proc/image/threshold.py +217 -0
  132. sigima/proc/image/transformations.py +393 -0
  133. sigima/proc/signal/__init__.py +376 -0
  134. sigima/proc/signal/analysis.py +206 -0
  135. sigima/proc/signal/arithmetic.py +551 -0
  136. sigima/proc/signal/base.py +262 -0
  137. sigima/proc/signal/extraction.py +60 -0
  138. sigima/proc/signal/features.py +310 -0
  139. sigima/proc/signal/filtering.py +484 -0
  140. sigima/proc/signal/fitting.py +276 -0
  141. sigima/proc/signal/fourier.py +259 -0
  142. sigima/proc/signal/mathops.py +420 -0
  143. sigima/proc/signal/processing.py +580 -0
  144. sigima/proc/signal/stability.py +175 -0
  145. sigima/proc/title_formatting.py +227 -0
  146. sigima/proc/validation.py +272 -0
  147. sigima/tests/__init__.py +7 -0
  148. sigima/tests/common/__init__.py +0 -0
  149. sigima/tests/common/arithmeticparam_unit_test.py +26 -0
  150. sigima/tests/common/basename_unit_test.py +126 -0
  151. sigima/tests/common/client_unit_test.py +412 -0
  152. sigima/tests/common/converters_unit_test.py +77 -0
  153. sigima/tests/common/decorator_unit_test.py +176 -0
  154. sigima/tests/common/examples_unit_test.py +104 -0
  155. sigima/tests/common/kernel_normalization_unit_test.py +242 -0
  156. sigima/tests/common/roi_basic_unit_test.py +73 -0
  157. sigima/tests/common/roi_geometry_unit_test.py +171 -0
  158. sigima/tests/common/scalar_builder_unit_test.py +142 -0
  159. sigima/tests/common/scalar_unit_test.py +991 -0
  160. sigima/tests/common/shape_unit_test.py +183 -0
  161. sigima/tests/common/stat_unit_test.py +138 -0
  162. sigima/tests/common/title_formatting_unit_test.py +338 -0
  163. sigima/tests/common/tools_coordinates_unit_test.py +60 -0
  164. sigima/tests/common/transformations_unit_test.py +178 -0
  165. sigima/tests/common/validation_unit_test.py +205 -0
  166. sigima/tests/conftest.py +129 -0
  167. sigima/tests/data.py +998 -0
  168. sigima/tests/env.py +280 -0
  169. sigima/tests/guiutils.py +163 -0
  170. sigima/tests/helpers.py +532 -0
  171. sigima/tests/image/__init__.py +28 -0
  172. sigima/tests/image/binning_unit_test.py +128 -0
  173. sigima/tests/image/blob_detection_unit_test.py +312 -0
  174. sigima/tests/image/centroid_unit_test.py +170 -0
  175. sigima/tests/image/check_2d_array_unit_test.py +63 -0
  176. sigima/tests/image/contour_unit_test.py +172 -0
  177. sigima/tests/image/convolution_unit_test.py +178 -0
  178. sigima/tests/image/datatype_unit_test.py +67 -0
  179. sigima/tests/image/edges_unit_test.py +155 -0
  180. sigima/tests/image/enclosingcircle_unit_test.py +88 -0
  181. sigima/tests/image/exposure_unit_test.py +223 -0
  182. sigima/tests/image/fft2d_unit_test.py +189 -0
  183. sigima/tests/image/filtering_unit_test.py +166 -0
  184. sigima/tests/image/geometry_unit_test.py +654 -0
  185. sigima/tests/image/hough_circle_unit_test.py +147 -0
  186. sigima/tests/image/imageobj_unit_test.py +737 -0
  187. sigima/tests/image/morphology_unit_test.py +71 -0
  188. sigima/tests/image/noise_unit_test.py +57 -0
  189. sigima/tests/image/offset_correction_unit_test.py +72 -0
  190. sigima/tests/image/operation_unit_test.py +518 -0
  191. sigima/tests/image/peak2d_limits_unit_test.py +41 -0
  192. sigima/tests/image/peak2d_unit_test.py +133 -0
  193. sigima/tests/image/profile_unit_test.py +159 -0
  194. sigima/tests/image/projections_unit_test.py +121 -0
  195. sigima/tests/image/restoration_unit_test.py +141 -0
  196. sigima/tests/image/roi2dparam_unit_test.py +53 -0
  197. sigima/tests/image/roi_advanced_unit_test.py +588 -0
  198. sigima/tests/image/roi_grid_unit_test.py +279 -0
  199. sigima/tests/image/spectrum2d_unit_test.py +40 -0
  200. sigima/tests/image/threshold_unit_test.py +91 -0
  201. sigima/tests/io/__init__.py +0 -0
  202. sigima/tests/io/addnewformat_unit_test.py +125 -0
  203. sigima/tests/io/convenience_funcs_unit_test.py +470 -0
  204. sigima/tests/io/coordinated_text_format_unit_test.py +495 -0
  205. sigima/tests/io/datetime_csv_unit_test.py +198 -0
  206. sigima/tests/io/imageio_formats_test.py +41 -0
  207. sigima/tests/io/ioregistry_unit_test.py +69 -0
  208. sigima/tests/io/objmeta_unit_test.py +87 -0
  209. sigima/tests/io/readobj_unit_test.py +130 -0
  210. sigima/tests/io/readwriteobj_unit_test.py +67 -0
  211. sigima/tests/signal/__init__.py +0 -0
  212. sigima/tests/signal/analysis_unit_test.py +135 -0
  213. sigima/tests/signal/check_1d_arrays_unit_test.py +169 -0
  214. sigima/tests/signal/convolution_unit_test.py +404 -0
  215. sigima/tests/signal/datetime_unit_test.py +176 -0
  216. sigima/tests/signal/fft1d_unit_test.py +303 -0
  217. sigima/tests/signal/filters_unit_test.py +403 -0
  218. sigima/tests/signal/fitting_unit_test.py +929 -0
  219. sigima/tests/signal/fwhm_unit_test.py +111 -0
  220. sigima/tests/signal/noise_unit_test.py +128 -0
  221. sigima/tests/signal/offset_correction_unit_test.py +34 -0
  222. sigima/tests/signal/operation_unit_test.py +489 -0
  223. sigima/tests/signal/peakdetection_unit_test.py +145 -0
  224. sigima/tests/signal/processing_unit_test.py +657 -0
  225. sigima/tests/signal/pulse/__init__.py +112 -0
  226. sigima/tests/signal/pulse/crossing_times_unit_test.py +123 -0
  227. sigima/tests/signal/pulse/plateau_detection_unit_test.py +102 -0
  228. sigima/tests/signal/pulse/pulse_unit_test.py +1824 -0
  229. sigima/tests/signal/roi_advanced_unit_test.py +392 -0
  230. sigima/tests/signal/signalobj_unit_test.py +603 -0
  231. sigima/tests/signal/stability_unit_test.py +431 -0
  232. sigima/tests/signal/uncertainty_unit_test.py +611 -0
  233. sigima/tests/vistools.py +1030 -0
  234. sigima/tools/__init__.py +59 -0
  235. sigima/tools/checks.py +290 -0
  236. sigima/tools/coordinates.py +308 -0
  237. sigima/tools/datatypes.py +26 -0
  238. sigima/tools/image/__init__.py +97 -0
  239. sigima/tools/image/detection.py +451 -0
  240. sigima/tools/image/exposure.py +77 -0
  241. sigima/tools/image/extraction.py +48 -0
  242. sigima/tools/image/fourier.py +260 -0
  243. sigima/tools/image/geometry.py +190 -0
  244. sigima/tools/image/preprocessing.py +165 -0
  245. sigima/tools/signal/__init__.py +86 -0
  246. sigima/tools/signal/dynamic.py +254 -0
  247. sigima/tools/signal/features.py +135 -0
  248. sigima/tools/signal/filtering.py +171 -0
  249. sigima/tools/signal/fitting.py +1171 -0
  250. sigima/tools/signal/fourier.py +466 -0
  251. sigima/tools/signal/interpolation.py +70 -0
  252. sigima/tools/signal/peakdetection.py +126 -0
  253. sigima/tools/signal/pulse.py +1626 -0
  254. sigima/tools/signal/scaling.py +50 -0
  255. sigima/tools/signal/stability.py +258 -0
  256. sigima/tools/signal/windowing.py +90 -0
  257. sigima/worker.py +79 -0
  258. sigima-1.0.0.dist-info/METADATA +233 -0
  259. sigima-1.0.0.dist-info/RECORD +262 -0
  260. {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/licenses/LICENSE +29 -29
  261. sigima-0.0.1.dev0.dist-info/METADATA +0 -60
  262. sigima-0.0.1.dev0.dist-info/RECORD +0 -6
  263. {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/WHEEL +0 -0
  264. {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,5 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Common I/O features
5
+ """
@@ -0,0 +1,164 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """Common functions for file name handling."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import re
8
+ import string
9
+ import sys
10
+ import unicodedata
11
+ from typing import Any, Iterable
12
+
13
+ from sigima.objects.image import ImageObj
14
+ from sigima.objects.signal import SignalObj
15
+
16
+
17
+ class CustomFormatter(string.Formatter):
18
+ """Custom string formatter to handle uppercase and lowercase strings."""
19
+
20
+ def format_field(self, value, format_spec):
21
+ """Format the given `value` according to the specified `format_spec`.
22
+
23
+ If the value is a string and the format_spec ends with 'upper' or 'lower',
24
+ convert the value to uppercase or lowercase, respectively, and remove the
25
+ suffix from `format_spec` before formatting.
26
+
27
+ Args:
28
+ value: Value to format.
29
+ format_spec: Format specification, may end with 'upper' or 'lower'.
30
+
31
+ Returns:
32
+ The formatted value.
33
+
34
+ Raises:
35
+ ValueError: If `format_spec` is invalid.
36
+ """
37
+ # Ignore dict objects silently (metadata should only be accessed via keys)
38
+ if isinstance(value, dict):
39
+ return ""
40
+ if isinstance(value, str):
41
+ if format_spec.endswith("upper"):
42
+ value = value.upper()
43
+ format_spec = format_spec[:-5]
44
+ elif format_spec.endswith("lower"):
45
+ value = value.lower()
46
+ format_spec = format_spec[:-5]
47
+ return super().format_field(value, format_spec)
48
+
49
+
50
+ def format_basenames(
51
+ objects: Iterable[SignalObj | ImageObj],
52
+ fmt: str,
53
+ replacement: str = "_",
54
+ ) -> list[str]:
55
+ """Generate sanitized filenames for SignalObj or ImageObj instances.
56
+
57
+ Format each object's name using the provided Python format string, then sanitize
58
+ the result for safe use as a filename. The format string may reference any of:
59
+ - {title}: object title
60
+ - {index}: 1-based index
61
+ - {count}: total number of objects
62
+ - {xlabel}, {xunit}, {ylabel}, {yunit}: axis labels/units (if present)
63
+ - {metadata[key]}: specific metadata value
64
+ (direct {metadata} use is silently ignored)
65
+
66
+ Args:
67
+ objects: Objects to name.
68
+ fmt: Python format string for naming.
69
+ replacement: Replacement for invalid filename characters.
70
+
71
+ Returns:
72
+ Sanitized filenames for each object.
73
+
74
+ Raises:
75
+ KeyError: If the format string references an unknown placeholder.
76
+ """
77
+ result: list[str] = []
78
+ formatter = CustomFormatter()
79
+ for i, obj in enumerate(objects):
80
+ # Note: We provide metadata dict only for {metadata[key]} access,
81
+ # not for direct {metadata} use (which would create overly long filenames)
82
+ metadata = getattr(obj, "metadata", {})
83
+ context: dict[str, Any] = {
84
+ "title": getattr(obj, "title", ""),
85
+ "index": i + 1,
86
+ "count": len(list(objects)),
87
+ # Attributes may not exist on all objects.
88
+ "xlabel": getattr(obj, "xlabel", ""),
89
+ "xunit": getattr(obj, "xunit", ""),
90
+ "ylabel": getattr(obj, "ylabel", ""),
91
+ "yunit": getattr(obj, "yunit", ""),
92
+ "metadata": metadata,
93
+ }
94
+ try:
95
+ formatted = formatter.format(fmt, **context)
96
+ except KeyError as exc:
97
+ missing = str(exc.args[0]) if exc.args else str(exc)
98
+ raise KeyError(f"Unknown format key in fmt: {missing!r}") from exc
99
+ except ValueError as exc:
100
+ # Re-raise with more context about which object failed
101
+ raise ValueError(
102
+ f"Invalid format string '{fmt}' for object '{context['title']}': {exc}"
103
+ ) from exc
104
+ # Sanitize final result to ensure it's a safe basename.
105
+ result.append(sanitize_basename(formatted, replacement=replacement))
106
+ return result
107
+
108
+
109
+ def sanitize_basename(basename: str, replacement: str = "_") -> str:
110
+ """Sanitize a string to create a valid basename for the current operating system.
111
+
112
+ This function removes or replaces characters that are invalid in basenames,
113
+ depending on the underlying OS (Windows, macOS, Linux). It also strips trailing dots
114
+ and spaces on Windows and normalizes unicode characters.
115
+
116
+ Args:
117
+ basename: Input string.
118
+ replacement: Replacement string for invalid characters (default: "_").
119
+
120
+ Returns:
121
+ A sanitized string that can safely be used as a basename.
122
+ """
123
+ # Normalize unicode characters (NFKD form for decomposing accents and the like).
124
+ basename = unicodedata.normalize("NFKD", basename)
125
+ basename = basename.encode("ascii", "ignore").decode("ascii")
126
+
127
+ # Characters not allowed in filenames (platform-dependent).
128
+ if sys.platform.startswith("win"):
129
+ # Reserved characters on Windows.
130
+ invalid_chars = r'[<>:"/\\|?*\x00-\x1F]'
131
+ reserved_names = {
132
+ "CON",
133
+ "PRN",
134
+ "AUX",
135
+ "NUL",
136
+ *(f"COM{i}" for i in range(1, 10)),
137
+ *(f"LPT{i}" for i in range(1, 10)),
138
+ }
139
+ else:
140
+ # Only '/' is disallowed on Unix-based systems.
141
+ invalid_chars = r"/"
142
+ reserved_names = set()
143
+
144
+ # Replace invalid characters.
145
+ sanitized = re.sub(invalid_chars, replacement, basename)
146
+
147
+ # Strip leading/trailing whitespace.
148
+ sanitized = sanitized.strip()
149
+ # On Windows, also strip trailing dots and spaces.
150
+ if sys.platform.startswith("win"):
151
+ sanitized = sanitized.rstrip(" .")
152
+
153
+ # Truncate to a reasonable length to avoid OS path issues.
154
+ sanitized = sanitized[:255]
155
+
156
+ # Avoid reserved basenames.
157
+ if sanitized.upper() in reserved_names:
158
+ sanitized += "_"
159
+
160
+ # If result is empty, fallback to a default name.
161
+ if not sanitized:
162
+ sanitized = "unnamed"
163
+
164
+ return sanitized
@@ -0,0 +1,189 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ I/O conversion functions
5
+ """
6
+
7
+ # pylint: disable=invalid-name # Allows short reference names like x, y, ...
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, Sequence
12
+
13
+ import numpy as np
14
+ import skimage
15
+
16
+
17
+ def dtypes_to_sorted_short_codes(
18
+ dtypes: Sequence[Any], kind_filter: str | None = None
19
+ ) -> list[str]:
20
+ """Return sorted short dtype codes for numeric dtypes.
21
+
22
+ Convert each input to a numpy dtype and ignore non-numeric types.
23
+ Order:
24
+ - Integer types first, unsigned (and boolean) before signed,
25
+ sorted by itemsize ascending.
26
+ - floats numeric types, sorted by itemsize ascending.
27
+ - complex numeric types, sorted by itemsize ascending.
28
+
29
+ Short codes use numpy kind letter plus itemsize in bytes, e.g. "u1", "i2",
30
+ "f8".
31
+
32
+ Args:
33
+ dtypes: Sequence of objects acceptable by numpy.dtype (dtype, str, etc.)
34
+ kind_filter: String of dtype kind letters to keep, e.g. "iu" for
35
+ unsigned/signed integers. If empty or None, keep all numeric types
36
+
37
+ Returns:
38
+ List of unique short dtype codes in the requested order.
39
+ """
40
+ dtypes = [np.dtype(d).str[1:] for d in dtypes]
41
+ ordered: list[np.dtype] = []
42
+
43
+ if kind_filter is None:
44
+ kind_filter = "iubfc" # all numeric types
45
+ assert kind_filter != "", "kind_filter cannot be empty string"
46
+
47
+ # Standard dtype codes in desired order
48
+ bool_codes = ("b1",)
49
+ int_codes = ("u1", "i1", "u2", "i2", "u4", "i4", "u8", "i8")
50
+ float_codes = ("f2", "f4", "f8")
51
+ complex_codes = ("c8", "c16")
52
+
53
+ ordered = [
54
+ code
55
+ for code in bool_codes + int_codes + float_codes + complex_codes
56
+ if code in dtypes and code[0] in kind_filter
57
+ ]
58
+ return ordered
59
+
60
+
61
+ def _convert_bool_array(array: np.ndarray) -> np.ndarray:
62
+ """Convert boolean array to uint8."""
63
+ return skimage.util.img_as_ubyte(array)
64
+
65
+
66
+ def _convert_int_array(
67
+ array: np.ndarray, supported_data_types: tuple[np.dtype]
68
+ ) -> np.ndarray:
69
+ """Convert an integer array to a standard type.
70
+
71
+ Select the smallest supported integer dtype that can represent all values in the
72
+ array. If no suitable integer dtype is found, convert the array to a supported
73
+ float type.
74
+
75
+ Args:
76
+ array: Input numpy array of integer type.
77
+ supported_data_types: Tuple of supported numpy dtypes for destination object.
78
+
79
+ Returns:
80
+ Converted numpy array with the selected dtype.
81
+
82
+ Raises:
83
+ ValueError: If no supported dtype can represent the data.
84
+ """
85
+ ordered_codes = dtypes_to_sorted_short_codes(supported_data_types, kind_filter="iu")
86
+
87
+ amin = np.min(array) if array.size > 0 else 0
88
+ amax = np.max(array) if array.size > 0 else 0
89
+ for code in ordered_codes:
90
+ info = np.iinfo(code)
91
+ if amin >= info.min and amax <= info.max:
92
+ new_type = np.dtype(code).newbyteorder("=")
93
+ break
94
+ else:
95
+ new_type = _convert_float_array(array, supported_data_types).dtype
96
+
97
+ return array.astype(new_type, copy=False)
98
+
99
+
100
+ def _convert_float_array(
101
+ array: np.ndarray, supported_data_types: tuple[np.dtype]
102
+ ) -> np.ndarray:
103
+ """Convert float/complex array to smallest allowed type at least large as current.
104
+
105
+ Choose the smallest supported dtype of the same kind ("f" for floats,
106
+ "c" for complex) whose itemsize is greater than or equal to the array's
107
+ itemsize. If no such type exists, fall back to the largest supported
108
+ dtype for that kind.
109
+
110
+ Args:
111
+ array: Array to convert.
112
+ supported_data_types: Sequence of allowed dtypes for the destination
113
+ object type.
114
+
115
+ Returns:
116
+ Converted array with the selected dtype. If no supported dtype of the
117
+ same kind exists, return the original array.
118
+ """
119
+ kind = array.dtype.kind
120
+ if kind in ["i", "u", "b"]:
121
+ kind = "f" # convert integers to floats
122
+
123
+ itemsize = array.dtype.itemsize
124
+
125
+ ordered_codes = dtypes_to_sorted_short_codes(supported_data_types, kind_filter=kind)
126
+
127
+ # Filter out any codes that don't match the requested kind (defensive).
128
+ valid_codes: list[str] = []
129
+ for code in ordered_codes:
130
+ try:
131
+ dt = np.dtype(code)
132
+ except TypeError:
133
+ continue
134
+ if dt.kind == kind:
135
+ valid_codes.append(code)
136
+
137
+ if not valid_codes:
138
+ # No supported dtype for this kind, return original array.
139
+ raise ValueError("Unsupported data type")
140
+
141
+ # Find smallest supported type with itemsize >= current itemsize.
142
+ selected_code: str | None = None
143
+ for code in valid_codes:
144
+ dt = np.dtype(code)
145
+ if dt.itemsize >= itemsize:
146
+ selected_code = code
147
+ break
148
+ else:
149
+ # Fallback to the largest supported type for this kind.
150
+ selected_code = valid_codes[-1]
151
+
152
+ new_type = np.dtype(selected_code).newbyteorder("=")
153
+ return array.astype(new_type, copy=False)
154
+
155
+
156
+ def convert_array_to_valid_dtype(
157
+ array: np.ndarray, valid_dtypes: tuple[np.dtype, ...]
158
+ ) -> np.ndarray:
159
+ """Convert array to the most appropriate valid dtype.
160
+
161
+ Converts arrays to one of the valid dtypes, choosing the most appropriate type
162
+ based on the input array's characteristics.
163
+
164
+ Args:
165
+ array: array to convert
166
+ valid_dtypes: tuple of valid dtypes
167
+
168
+ Returns:
169
+ Converted array with the most appropriate valid dtype.
170
+
171
+ Raises:
172
+ TypeError: if input is not a numpy ndarray
173
+ ValueError: if array dtype cannot be converted to any valid type
174
+ """
175
+ if not isinstance(array, np.ndarray):
176
+ raise TypeError("Input must be a numpy ndarray.")
177
+
178
+ if array.dtype in valid_dtypes:
179
+ return array
180
+
181
+ kind: str = array.dtype.kind
182
+ if kind in ["f", "c"]:
183
+ return _convert_float_array(array, valid_dtypes)
184
+ if kind == "b":
185
+ return _convert_bool_array(array)
186
+ if kind in ["i", "u"]:
187
+ return _convert_int_array(array, valid_dtypes)
188
+
189
+ raise ValueError("Unsupported data type")
@@ -0,0 +1,181 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """Sigima I/O module for handling object metadata and ROIs."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import TYPE_CHECKING, Any, Literal
8
+
9
+ from guidata.io import JSONHandler, JSONReader, JSONWriter
10
+
11
+ from sigima.objects import ImageROI, SignalROI
12
+
13
+ if TYPE_CHECKING:
14
+ from sigima.params import ROIGridParam
15
+
16
+ FORMAT_TAG = "sigima"
17
+ FORMAT_VERSION = "1.0"
18
+ ROI_TYPE_FIELD = "roi_type"
19
+
20
+
21
+ def _check_tag(data: dict, expected_format: str) -> None:
22
+ """Validate the presence and type of sigima tag.
23
+
24
+ Args:
25
+ data: The data dictionary to check.
26
+ expected_format: The expected format string for the tag.
27
+
28
+ Raises:
29
+ ValueError: If the tag is missing or does not match the expected format.
30
+ """
31
+ tag: dict = data.get(FORMAT_TAG, {})
32
+ if tag.get("format") != expected_format:
33
+ raise ValueError(f"Unexpected or missing format: {tag}")
34
+
35
+
36
+ def write_dict(filepath: str, data: dict) -> None:
37
+ """Write a dictionary to a file in JSON format.
38
+
39
+ Args:
40
+ filepath: The file path to write the data to.
41
+ data: The dictionary to serialize.
42
+
43
+ Raises:
44
+ ValueError: If the data is not a dictionary.
45
+ """
46
+ if not isinstance(data, dict):
47
+ raise ValueError(f"Expected a dictionary, got {type(data)}")
48
+ handler = JSONHandler(filepath)
49
+ handler.set_json_dict(data)
50
+ handler.save()
51
+
52
+
53
+ def read_dict(filepath: str) -> dict:
54
+ """Read a dictionary from a file and return it.
55
+
56
+ Args:
57
+ filepath: The file path to read the data from.
58
+
59
+ Returns:
60
+ The dictionary read from the file.
61
+ """
62
+ handler = JSONHandler(filepath)
63
+ handler.load()
64
+ data = handler.get_json_dict()
65
+ return data
66
+
67
+
68
+ def write_roi(filepath: str, roi: SignalROI | ImageROI) -> None:
69
+ """Write a signal or image ROI to a file in JSON format.
70
+
71
+ Args:
72
+ filepath: The file path to write the ROI data to.
73
+ roi: The signal or image ROI object to serialize.
74
+
75
+ Raises:
76
+ ValueError: If the ROI object is not of type SignalROI or ImageROI.
77
+ """
78
+ if isinstance(roi, SignalROI):
79
+ roi_type: Literal["signal", "image"] = "signal"
80
+ elif isinstance(roi, ImageROI):
81
+ roi_type = "image"
82
+ else:
83
+ raise ValueError(
84
+ f"Unsupported ROI type: {type(roi)}. Expected SignalROI or ImageROI."
85
+ )
86
+ roi_dict = roi.to_dict()
87
+ roi_dict[ROI_TYPE_FIELD] = roi_type
88
+ data = {
89
+ FORMAT_TAG: {"format": "roi", "version": FORMAT_VERSION},
90
+ "roi": roi_dict,
91
+ }
92
+ write_dict(filepath, data)
93
+
94
+
95
+ def read_roi(filepath: str) -> SignalROI | ImageROI:
96
+ """Read ROI data from a file and return the corresponding ROI object.
97
+
98
+ Args:
99
+ filepath: The file path to read the ROI data from.
100
+
101
+ Returns:
102
+ The corresponding ROI object (SignalROI or ImageROI).
103
+
104
+ Raises:
105
+ ValueError: If the file does not contain the expected format.
106
+ """
107
+ json_dict = read_dict(filepath)
108
+ _check_tag(json_dict, expected_format="roi")
109
+ roi_dict = json_dict["roi"]
110
+ assert isinstance(roi_dict, dict), "ROI data must be a dictionary"
111
+ roi_type = roi_dict.pop(ROI_TYPE_FIELD, None)
112
+ if roi_type == "signal":
113
+ return SignalROI.from_dict(roi_dict)
114
+ if roi_type == "image":
115
+ return ImageROI.from_dict(roi_dict)
116
+ raise ValueError(f"Unsupported or missing ROI type: {roi_type}")
117
+
118
+
119
+ def write_roi_grid(filepath: str, param: ROIGridParam) -> None:
120
+ """Write ROI grid parameters to a file in JSON format.
121
+
122
+ Args:
123
+ filepath: The file path to write the ROI grid parameters to.
124
+ param: The ROI grid parameters to serialize.
125
+ """
126
+ writer = JSONWriter(filepath)
127
+ param.serialize(writer)
128
+ print(writer.jsondata)
129
+ writer.save()
130
+
131
+
132
+ def read_roi_grid(filepath: str) -> ROIGridParam:
133
+ """Read ROI grid parameters from a file in JSON format.
134
+
135
+ Args:
136
+ filepath: The file path to read the ROI grid parameters from.
137
+
138
+ Returns:
139
+ The ROI grid parameters read from the file.
140
+ """
141
+ from sigima.params import ROIGridParam # pylint: disable=import-outside-toplevel
142
+
143
+ handler = JSONReader(filepath)
144
+ handler.load()
145
+ param = ROIGridParam()
146
+ param.deserialize(handler)
147
+ return param
148
+
149
+
150
+ def write_metadata(filepath: str, metadata: dict[str, Any]) -> None:
151
+ """Write metadata to a file in JSON format.
152
+
153
+ Args:
154
+ filepath: The file path to write the metadata to.
155
+ metadata: The metadata dictionary to serialize.
156
+
157
+ Raises:
158
+ ValueError: If the object does not have a metadata attribute.
159
+ """
160
+ data = {
161
+ FORMAT_TAG: {"format": "metadata", "version": FORMAT_VERSION},
162
+ "metadata": metadata.copy(),
163
+ }
164
+ write_dict(filepath, data)
165
+
166
+
167
+ def read_metadata(filepath: str) -> dict[str, Any]:
168
+ """Read metadata from a file and return the metadata dictionary.
169
+
170
+ Args:
171
+ filepath: The file path to read the metadata from.
172
+
173
+ Returns:
174
+ The metadata dictionary.
175
+
176
+ Raises:
177
+ ValueError: If the file does not contain the expected format.
178
+ """
179
+ json_dict = read_dict(filepath)
180
+ _check_tag(json_dict, expected_format="metadata")
181
+ return json_dict["metadata"]
@@ -0,0 +1,58 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """I/O utility functions."""
4
+
5
+ # pylint: disable=invalid-name # Allows short reference names like x, y...
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from itertools import islice
11
+
12
+ from sigima.io.enums import FileEncoding
13
+
14
+
15
+ def count_lines(filename: str | os.PathLike[str]) -> int:
16
+ """Count the number of lines in a file.
17
+
18
+ Args:
19
+ filename: File name or path.
20
+
21
+ Returns:
22
+ The number of lines in the file.
23
+
24
+ Raises:
25
+ IOError: If the file cannot be read.
26
+ """
27
+ for encoding in FileEncoding:
28
+ try:
29
+ with open(filename, "r", encoding=encoding) as file:
30
+ line_count = sum(1 for _ in file)
31
+ return line_count
32
+ except UnicodeDecodeError:
33
+ # Try next encoding.
34
+ pass
35
+ raise IOError(f"Cannot read file {filename}.")
36
+
37
+
38
+ def read_first_n_lines(filename: str | os.PathLike[str], n: int = 100000) -> str:
39
+ """Read the first `n` lines of a file.
40
+
41
+ Args:
42
+ filename: File name or path.
43
+ n: Number of lines to read.
44
+
45
+ Returns:
46
+ The first `n` lines of the file.
47
+
48
+ Raises:
49
+ IOError: If the file cannot be read.
50
+ """
51
+ for encoding in FileEncoding:
52
+ try:
53
+ with open(filename, "r", encoding=encoding) as file:
54
+ return "".join(islice(file, n))
55
+ except UnicodeDecodeError:
56
+ # Try next encoding.
57
+ pass
58
+ raise IOError(f"Cannot read file {filename}.")