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.
- sigima/__init__.py +142 -2
- sigima/client/__init__.py +105 -0
- sigima/client/base.py +780 -0
- sigima/client/remote.py +469 -0
- sigima/client/stub.py +814 -0
- sigima/client/utils.py +90 -0
- sigima/config.py +444 -0
- sigima/data/logo/Sigima.svg +135 -0
- sigima/data/tests/annotations.json +798 -0
- sigima/data/tests/curve_fitting/exponential_fit.txt +511 -0
- sigima/data/tests/curve_fitting/gaussian_fit.txt +100 -0
- sigima/data/tests/curve_fitting/piecewiseexponential_fit.txt +1022 -0
- sigima/data/tests/curve_fitting/polynomial_fit.txt +100 -0
- sigima/data/tests/curve_fitting/twohalfgaussian_fit.txt +1000 -0
- sigima/data/tests/curve_formats/bandwidth.txt +201 -0
- sigima/data/tests/curve_formats/boxcar.npy +0 -0
- sigima/data/tests/curve_formats/datetime.txt +1001 -0
- sigima/data/tests/curve_formats/dynamic_parameters.txt +4000 -0
- sigima/data/tests/curve_formats/fw1e2.txt +301 -0
- sigima/data/tests/curve_formats/fwhm.txt +319 -0
- sigima/data/tests/curve_formats/multiple_curves.csv +29 -0
- sigima/data/tests/curve_formats/noised_saw.mat +0 -0
- sigima/data/tests/curve_formats/oscilloscope.csv +111 -0
- sigima/data/tests/curve_formats/other/other2/recursive2.txt +5 -0
- sigima/data/tests/curve_formats/other/recursive1.txt +5 -0
- sigima/data/tests/curve_formats/paracetamol.npy +0 -0
- sigima/data/tests/curve_formats/paracetamol.txt +1010 -0
- sigima/data/tests/curve_formats/paracetamol_dx_dy.csv +1000 -0
- sigima/data/tests/curve_formats/paracetamol_dy.csv +1001 -0
- sigima/data/tests/curve_formats/pulse1.npy +0 -0
- sigima/data/tests/curve_formats/pulse2.npy +0 -0
- sigima/data/tests/curve_formats/simple.txt +5 -0
- sigima/data/tests/curve_formats/spectrum.mca +2139 -0
- sigima/data/tests/curve_formats/square2.npy +0 -0
- sigima/data/tests/curve_formats/step.npy +0 -0
- sigima/data/tests/fabry-perot1.jpg +0 -0
- sigima/data/tests/fabry-perot2.jpg +0 -0
- sigima/data/tests/flower.npy +0 -0
- sigima/data/tests/image_formats/NF 180338201.scor-data +11003 -0
- sigima/data/tests/image_formats/binary_image.npy +0 -0
- sigima/data/tests/image_formats/binary_image.png +0 -0
- sigima/data/tests/image_formats/centroid_test.npy +0 -0
- sigima/data/tests/image_formats/coordinated_text/complex_image.txt +10011 -0
- sigima/data/tests/image_formats/coordinated_text/complex_ref_image.txt +10010 -0
- sigima/data/tests/image_formats/coordinated_text/image.txt +15 -0
- sigima/data/tests/image_formats/coordinated_text/image2.txt +14 -0
- sigima/data/tests/image_formats/coordinated_text/image_no_unit_no_label.txt +14 -0
- sigima/data/tests/image_formats/coordinated_text/image_with_nan.txt +15 -0
- sigima/data/tests/image_formats/coordinated_text/image_with_unit.txt +14 -0
- sigima/data/tests/image_formats/fiber.csv +480 -0
- sigima/data/tests/image_formats/fiber.jpg +0 -0
- sigima/data/tests/image_formats/fiber.png +0 -0
- sigima/data/tests/image_formats/fiber.txt +480 -0
- sigima/data/tests/image_formats/gaussian_spot_with_noise.npy +0 -0
- sigima/data/tests/image_formats/mr-brain.dcm +0 -0
- sigima/data/tests/image_formats/noised_gaussian.mat +0 -0
- sigima/data/tests/image_formats/sif_reader/nd_lum_image_no_glue.sif +0 -0
- sigima/data/tests/image_formats/sif_reader/raman1.sif +0 -0
- sigima/data/tests/image_formats/tiling.txt +10 -0
- sigima/data/tests/image_formats/uint16.tiff +0 -0
- sigima/data/tests/image_formats/uint8.tiff +0 -0
- sigima/data/tests/laser_beam/TEM00_z_13.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_18.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_23.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_30.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_35.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_40.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_45.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_50.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_55.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_60.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_65.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_70.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_75.jpg +0 -0
- sigima/data/tests/laser_beam/TEM00_z_80.jpg +0 -0
- sigima/enums.py +195 -0
- sigima/io/__init__.py +123 -0
- sigima/io/base.py +311 -0
- sigima/io/common/__init__.py +5 -0
- sigima/io/common/basename.py +164 -0
- sigima/io/common/converters.py +189 -0
- sigima/io/common/objmeta.py +181 -0
- sigima/io/common/textreader.py +58 -0
- sigima/io/convenience.py +157 -0
- sigima/io/enums.py +17 -0
- sigima/io/ftlab.py +395 -0
- sigima/io/image/__init__.py +9 -0
- sigima/io/image/base.py +177 -0
- sigima/io/image/formats.py +1016 -0
- sigima/io/image/funcs.py +414 -0
- sigima/io/signal/__init__.py +9 -0
- sigima/io/signal/base.py +129 -0
- sigima/io/signal/formats.py +290 -0
- sigima/io/signal/funcs.py +723 -0
- sigima/objects/__init__.py +260 -0
- sigima/objects/base.py +937 -0
- sigima/objects/image/__init__.py +88 -0
- sigima/objects/image/creation.py +556 -0
- sigima/objects/image/object.py +524 -0
- sigima/objects/image/roi.py +904 -0
- sigima/objects/scalar/__init__.py +57 -0
- sigima/objects/scalar/common.py +215 -0
- sigima/objects/scalar/geometry.py +502 -0
- sigima/objects/scalar/table.py +784 -0
- sigima/objects/shape.py +290 -0
- sigima/objects/signal/__init__.py +133 -0
- sigima/objects/signal/constants.py +27 -0
- sigima/objects/signal/creation.py +1428 -0
- sigima/objects/signal/object.py +444 -0
- sigima/objects/signal/roi.py +274 -0
- sigima/params.py +405 -0
- sigima/proc/__init__.py +96 -0
- sigima/proc/base.py +381 -0
- sigima/proc/decorator.py +330 -0
- sigima/proc/image/__init__.py +513 -0
- sigima/proc/image/arithmetic.py +335 -0
- sigima/proc/image/base.py +260 -0
- sigima/proc/image/detection.py +519 -0
- sigima/proc/image/edges.py +329 -0
- sigima/proc/image/exposure.py +406 -0
- sigima/proc/image/extraction.py +458 -0
- sigima/proc/image/filtering.py +219 -0
- sigima/proc/image/fourier.py +147 -0
- sigima/proc/image/geometry.py +661 -0
- sigima/proc/image/mathops.py +340 -0
- sigima/proc/image/measurement.py +195 -0
- sigima/proc/image/morphology.py +155 -0
- sigima/proc/image/noise.py +107 -0
- sigima/proc/image/preprocessing.py +182 -0
- sigima/proc/image/restoration.py +235 -0
- sigima/proc/image/threshold.py +217 -0
- sigima/proc/image/transformations.py +393 -0
- sigima/proc/signal/__init__.py +376 -0
- sigima/proc/signal/analysis.py +206 -0
- sigima/proc/signal/arithmetic.py +551 -0
- sigima/proc/signal/base.py +262 -0
- sigima/proc/signal/extraction.py +60 -0
- sigima/proc/signal/features.py +310 -0
- sigima/proc/signal/filtering.py +484 -0
- sigima/proc/signal/fitting.py +276 -0
- sigima/proc/signal/fourier.py +259 -0
- sigima/proc/signal/mathops.py +420 -0
- sigima/proc/signal/processing.py +580 -0
- sigima/proc/signal/stability.py +175 -0
- sigima/proc/title_formatting.py +227 -0
- sigima/proc/validation.py +272 -0
- sigima/tests/__init__.py +7 -0
- sigima/tests/common/__init__.py +0 -0
- sigima/tests/common/arithmeticparam_unit_test.py +26 -0
- sigima/tests/common/basename_unit_test.py +126 -0
- sigima/tests/common/client_unit_test.py +412 -0
- sigima/tests/common/converters_unit_test.py +77 -0
- sigima/tests/common/decorator_unit_test.py +176 -0
- sigima/tests/common/examples_unit_test.py +104 -0
- sigima/tests/common/kernel_normalization_unit_test.py +242 -0
- sigima/tests/common/roi_basic_unit_test.py +73 -0
- sigima/tests/common/roi_geometry_unit_test.py +171 -0
- sigima/tests/common/scalar_builder_unit_test.py +142 -0
- sigima/tests/common/scalar_unit_test.py +991 -0
- sigima/tests/common/shape_unit_test.py +183 -0
- sigima/tests/common/stat_unit_test.py +138 -0
- sigima/tests/common/title_formatting_unit_test.py +338 -0
- sigima/tests/common/tools_coordinates_unit_test.py +60 -0
- sigima/tests/common/transformations_unit_test.py +178 -0
- sigima/tests/common/validation_unit_test.py +205 -0
- sigima/tests/conftest.py +129 -0
- sigima/tests/data.py +998 -0
- sigima/tests/env.py +280 -0
- sigima/tests/guiutils.py +163 -0
- sigima/tests/helpers.py +532 -0
- sigima/tests/image/__init__.py +28 -0
- sigima/tests/image/binning_unit_test.py +128 -0
- sigima/tests/image/blob_detection_unit_test.py +312 -0
- sigima/tests/image/centroid_unit_test.py +170 -0
- sigima/tests/image/check_2d_array_unit_test.py +63 -0
- sigima/tests/image/contour_unit_test.py +172 -0
- sigima/tests/image/convolution_unit_test.py +178 -0
- sigima/tests/image/datatype_unit_test.py +67 -0
- sigima/tests/image/edges_unit_test.py +155 -0
- sigima/tests/image/enclosingcircle_unit_test.py +88 -0
- sigima/tests/image/exposure_unit_test.py +223 -0
- sigima/tests/image/fft2d_unit_test.py +189 -0
- sigima/tests/image/filtering_unit_test.py +166 -0
- sigima/tests/image/geometry_unit_test.py +654 -0
- sigima/tests/image/hough_circle_unit_test.py +147 -0
- sigima/tests/image/imageobj_unit_test.py +737 -0
- sigima/tests/image/morphology_unit_test.py +71 -0
- sigima/tests/image/noise_unit_test.py +57 -0
- sigima/tests/image/offset_correction_unit_test.py +72 -0
- sigima/tests/image/operation_unit_test.py +518 -0
- sigima/tests/image/peak2d_limits_unit_test.py +41 -0
- sigima/tests/image/peak2d_unit_test.py +133 -0
- sigima/tests/image/profile_unit_test.py +159 -0
- sigima/tests/image/projections_unit_test.py +121 -0
- sigima/tests/image/restoration_unit_test.py +141 -0
- sigima/tests/image/roi2dparam_unit_test.py +53 -0
- sigima/tests/image/roi_advanced_unit_test.py +588 -0
- sigima/tests/image/roi_grid_unit_test.py +279 -0
- sigima/tests/image/spectrum2d_unit_test.py +40 -0
- sigima/tests/image/threshold_unit_test.py +91 -0
- sigima/tests/io/__init__.py +0 -0
- sigima/tests/io/addnewformat_unit_test.py +125 -0
- sigima/tests/io/convenience_funcs_unit_test.py +470 -0
- sigima/tests/io/coordinated_text_format_unit_test.py +495 -0
- sigima/tests/io/datetime_csv_unit_test.py +198 -0
- sigima/tests/io/imageio_formats_test.py +41 -0
- sigima/tests/io/ioregistry_unit_test.py +69 -0
- sigima/tests/io/objmeta_unit_test.py +87 -0
- sigima/tests/io/readobj_unit_test.py +130 -0
- sigima/tests/io/readwriteobj_unit_test.py +67 -0
- sigima/tests/signal/__init__.py +0 -0
- sigima/tests/signal/analysis_unit_test.py +135 -0
- sigima/tests/signal/check_1d_arrays_unit_test.py +169 -0
- sigima/tests/signal/convolution_unit_test.py +404 -0
- sigima/tests/signal/datetime_unit_test.py +176 -0
- sigima/tests/signal/fft1d_unit_test.py +303 -0
- sigima/tests/signal/filters_unit_test.py +403 -0
- sigima/tests/signal/fitting_unit_test.py +929 -0
- sigima/tests/signal/fwhm_unit_test.py +111 -0
- sigima/tests/signal/noise_unit_test.py +128 -0
- sigima/tests/signal/offset_correction_unit_test.py +34 -0
- sigima/tests/signal/operation_unit_test.py +489 -0
- sigima/tests/signal/peakdetection_unit_test.py +145 -0
- sigima/tests/signal/processing_unit_test.py +657 -0
- sigima/tests/signal/pulse/__init__.py +112 -0
- sigima/tests/signal/pulse/crossing_times_unit_test.py +123 -0
- sigima/tests/signal/pulse/plateau_detection_unit_test.py +102 -0
- sigima/tests/signal/pulse/pulse_unit_test.py +1824 -0
- sigima/tests/signal/roi_advanced_unit_test.py +392 -0
- sigima/tests/signal/signalobj_unit_test.py +603 -0
- sigima/tests/signal/stability_unit_test.py +431 -0
- sigima/tests/signal/uncertainty_unit_test.py +611 -0
- sigima/tests/vistools.py +1030 -0
- sigima/tools/__init__.py +59 -0
- sigima/tools/checks.py +290 -0
- sigima/tools/coordinates.py +308 -0
- sigima/tools/datatypes.py +26 -0
- sigima/tools/image/__init__.py +97 -0
- sigima/tools/image/detection.py +451 -0
- sigima/tools/image/exposure.py +77 -0
- sigima/tools/image/extraction.py +48 -0
- sigima/tools/image/fourier.py +260 -0
- sigima/tools/image/geometry.py +190 -0
- sigima/tools/image/preprocessing.py +165 -0
- sigima/tools/signal/__init__.py +86 -0
- sigima/tools/signal/dynamic.py +254 -0
- sigima/tools/signal/features.py +135 -0
- sigima/tools/signal/filtering.py +171 -0
- sigima/tools/signal/fitting.py +1171 -0
- sigima/tools/signal/fourier.py +466 -0
- sigima/tools/signal/interpolation.py +70 -0
- sigima/tools/signal/peakdetection.py +126 -0
- sigima/tools/signal/pulse.py +1626 -0
- sigima/tools/signal/scaling.py +50 -0
- sigima/tools/signal/stability.py +258 -0
- sigima/tools/signal/windowing.py +90 -0
- sigima/worker.py +79 -0
- sigima-1.0.0.dist-info/METADATA +233 -0
- sigima-1.0.0.dist-info/RECORD +262 -0
- {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/licenses/LICENSE +29 -29
- sigima-0.0.1.dev0.dist-info/METADATA +0 -60
- sigima-0.0.1.dev0.dist-info/RECORD +0 -6
- {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/WHEEL +0 -0
- {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1428 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Signal creation utilities
|
|
5
|
+
========================
|
|
6
|
+
|
|
7
|
+
This module provides functions and parameter classes for creating new signals.
|
|
8
|
+
|
|
9
|
+
The module includes:
|
|
10
|
+
|
|
11
|
+
- `create_signal_from_param`: Factory function for creating SignalObj instances
|
|
12
|
+
from parameters
|
|
13
|
+
- `SignalTypes`: Enumeration of supported signal generation types
|
|
14
|
+
- `NewSignalParam` and subclasses: Parameter classes for signal generation
|
|
15
|
+
- Factory functions and registration utilities
|
|
16
|
+
|
|
17
|
+
These utilities support creating signals from various sources:
|
|
18
|
+
- Synthetic data (zeros, random distributions, analytical functions)
|
|
19
|
+
- Periodic functions (sine, cosine, square, etc.)
|
|
20
|
+
- Step functions, chirps, pulses
|
|
21
|
+
- Custom user-defined signals
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
|
|
25
|
+
# pylint: disable=duplicate-code
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import enum
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from typing import Literal, Type
|
|
32
|
+
|
|
33
|
+
import guidata.dataset as gds
|
|
34
|
+
import numpy as np
|
|
35
|
+
import scipy.constants
|
|
36
|
+
import scipy.signal as sps
|
|
37
|
+
|
|
38
|
+
from sigima.config import _
|
|
39
|
+
from sigima.enums import SignalShape
|
|
40
|
+
from sigima.objects import base
|
|
41
|
+
from sigima.objects.signal.object import SignalObj
|
|
42
|
+
from sigima.tools.signal.pulse import GaussianModel, LorentzianModel, VoigtModel
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def create_signal(
|
|
46
|
+
title: str,
|
|
47
|
+
x: np.ndarray | None = None,
|
|
48
|
+
y: np.ndarray | None = None,
|
|
49
|
+
dx: np.ndarray | None = None,
|
|
50
|
+
dy: np.ndarray | None = None,
|
|
51
|
+
metadata: dict | None = None,
|
|
52
|
+
units: tuple[str, str] | None = None,
|
|
53
|
+
labels: tuple[str, str] | None = None,
|
|
54
|
+
) -> SignalObj:
|
|
55
|
+
"""Create a new Signal object.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
title: signal title
|
|
59
|
+
x: X data
|
|
60
|
+
y: Y data
|
|
61
|
+
dx: dX data (optional: error bars)
|
|
62
|
+
dy: dY data (optional: error bars)
|
|
63
|
+
metadata: signal metadata
|
|
64
|
+
units: X, Y units (tuple of strings)
|
|
65
|
+
labels: X, Y labels (tuple of strings)
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Signal object
|
|
69
|
+
"""
|
|
70
|
+
assert isinstance(title, str)
|
|
71
|
+
signal = SignalObj(title=title)
|
|
72
|
+
signal.title = title
|
|
73
|
+
signal.set_xydata(x, y, dx=dx, dy=dy)
|
|
74
|
+
if units is not None:
|
|
75
|
+
signal.xunit, signal.yunit = units
|
|
76
|
+
if labels is not None:
|
|
77
|
+
signal.xlabel, signal.ylabel = labels
|
|
78
|
+
if metadata is not None:
|
|
79
|
+
signal.metadata.update(metadata)
|
|
80
|
+
return signal
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class SignalTypes(gds.LabeledEnum):
|
|
84
|
+
"""Signal types"""
|
|
85
|
+
|
|
86
|
+
#: Signal filled with zero
|
|
87
|
+
ZERO = "zero", _("Zero")
|
|
88
|
+
#: Random signal (normal distribution)
|
|
89
|
+
NORMAL_DISTRIBUTION = "normal_distribution", _("Normal distribution")
|
|
90
|
+
#: Random signal (Poisson distribution)
|
|
91
|
+
POISSON_DISTRIBUTION = "poisson_distribution", _("Poisson distribution")
|
|
92
|
+
#: Random signal (uniform distribution)
|
|
93
|
+
UNIFORM_DISTRIBUTION = "uniform_distribution", _("Uniform distribution")
|
|
94
|
+
#: Gaussian function
|
|
95
|
+
GAUSS = "gauss", _("Gaussian")
|
|
96
|
+
#: Lorentzian function
|
|
97
|
+
LORENTZ = "lorentz", _("Lorentzian")
|
|
98
|
+
#: Voigt function
|
|
99
|
+
VOIGT = "voigt", _("Voigt")
|
|
100
|
+
#: Planck function
|
|
101
|
+
PLANCK = "planck", _("Blackbody (Planck)")
|
|
102
|
+
#: Sinusoid
|
|
103
|
+
SINE = "sine", _("Sine")
|
|
104
|
+
#: Cosinusoid
|
|
105
|
+
COSINE = "cosine", _("Cosine")
|
|
106
|
+
#: Sawtooth function
|
|
107
|
+
SAWTOOTH = "sawtooth", _("Sawtooth")
|
|
108
|
+
#: Triangle function
|
|
109
|
+
TRIANGLE = "triangle", _("Triangle")
|
|
110
|
+
#: Square function
|
|
111
|
+
SQUARE = "square", _("Square")
|
|
112
|
+
#: Cardinal sine
|
|
113
|
+
SINC = "sinc", _("Cardinal sine")
|
|
114
|
+
#: Linear chirp
|
|
115
|
+
LINEARCHIRP = "linearchirp", _("Linear chirp")
|
|
116
|
+
#: Step function
|
|
117
|
+
STEP = "step", _("Step")
|
|
118
|
+
#: Exponential function
|
|
119
|
+
EXPONENTIAL = "exponential", _("Exponential")
|
|
120
|
+
#: Logistic function
|
|
121
|
+
LOGISTIC = "logistic", _("Logistic")
|
|
122
|
+
#: Pulse function
|
|
123
|
+
PULSE = "pulse", _("Pulse")
|
|
124
|
+
#: Step pulse function (with configurable rise time)
|
|
125
|
+
STEP_PULSE = "step_pulse", _("Step pulse")
|
|
126
|
+
#: Square pulse function (with configurable rise/fall times)
|
|
127
|
+
SQUARE_PULSE = "square_pulse", _("Square pulse")
|
|
128
|
+
#: Polynomial function
|
|
129
|
+
POLYNOMIAL = "polynomial", _("Polynomial")
|
|
130
|
+
#: Custom function
|
|
131
|
+
CUSTOM = "custom", _("Custom")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
DEFAULT_TITLE = _("Untitled signal")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class NewSignalParam(gds.DataSet):
|
|
138
|
+
"""New signal dataset.
|
|
139
|
+
|
|
140
|
+
Subclasses can optionally implement a ``generate_title()`` method to provide
|
|
141
|
+
automatic title generation based on their parameters. This method should return
|
|
142
|
+
a string containing the generated title, or an empty string if no title can be
|
|
143
|
+
generated.
|
|
144
|
+
|
|
145
|
+
Example::
|
|
146
|
+
|
|
147
|
+
def generate_title(self) -> str:
|
|
148
|
+
'''Generate a title based on current parameters.'''
|
|
149
|
+
return f"MySignal(param1={self.param1},param2={self.param2})"
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
title = gds.StringItem(_("Title"), default=DEFAULT_TITLE)
|
|
153
|
+
size = gds.IntItem(
|
|
154
|
+
_("N<sub>points</sub>"),
|
|
155
|
+
help=_("Total number of points in the signal"),
|
|
156
|
+
min=1,
|
|
157
|
+
default=500,
|
|
158
|
+
)
|
|
159
|
+
xmin = gds.FloatItem("x<sub>min</sub>", default=-10.0)
|
|
160
|
+
xmax = gds.FloatItem("x<sub>max</sub>", default=10.0).set_prop("display", col=1)
|
|
161
|
+
xlabel = gds.StringItem(_("X label"), default="")
|
|
162
|
+
xunit = gds.StringItem(_("X unit"), default="").set_prop("display", col=1)
|
|
163
|
+
ylabel = gds.StringItem(_("Y label"), default="")
|
|
164
|
+
yunit = gds.StringItem(_("Y unit"), default="").set_prop("display", col=1)
|
|
165
|
+
|
|
166
|
+
# As it is the last item of the dataset, the separator will be hidden if no other
|
|
167
|
+
# items are present after it (i.e. when derived classes do not add any new items
|
|
168
|
+
# or when the NewSignalParam class is used alone).
|
|
169
|
+
sep = gds.SeparatorItem()
|
|
170
|
+
|
|
171
|
+
def generate_x_data(self) -> np.ndarray:
|
|
172
|
+
"""Generate x data based on current parameters."""
|
|
173
|
+
return np.linspace(self.xmin, self.xmax, self.size)
|
|
174
|
+
|
|
175
|
+
def generate_1d_data(self) -> tuple[np.ndarray, np.ndarray]:
|
|
176
|
+
"""Compute 1D data based on current parameters.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Tuple of (x, y) arrays
|
|
180
|
+
"""
|
|
181
|
+
return self.generate_x_data(), np.zeros(self.size)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
SIGNAL_TYPE_PARAM_CLASSES = {}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def register_signal_parameters_class(stype: SignalTypes, param_class) -> None:
|
|
188
|
+
"""Register a parameters class for a given signal type.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
stype: signal type
|
|
192
|
+
param_class: parameters class
|
|
193
|
+
"""
|
|
194
|
+
SIGNAL_TYPE_PARAM_CLASSES[stype] = param_class
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def __get_signal_parameters_class(stype: SignalTypes) -> Type[NewSignalParam]:
|
|
198
|
+
"""Get parameters class for a given signal type.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
stype: signal type
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Parameters class
|
|
205
|
+
|
|
206
|
+
Raises:
|
|
207
|
+
ValueError: if no parameters class is registered for the given signal type
|
|
208
|
+
"""
|
|
209
|
+
try:
|
|
210
|
+
return SIGNAL_TYPE_PARAM_CLASSES[stype]
|
|
211
|
+
except KeyError as exc:
|
|
212
|
+
raise ValueError(
|
|
213
|
+
f"Image type {stype} has no parameters class registered"
|
|
214
|
+
) from exc
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def check_all_signal_parameters_classes() -> None:
|
|
218
|
+
"""Check all registered parameters classes."""
|
|
219
|
+
for stype, param_class in SIGNAL_TYPE_PARAM_CLASSES.items():
|
|
220
|
+
assert __get_signal_parameters_class(stype) is param_class
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def create_signal_parameters(
|
|
224
|
+
stype: SignalTypes,
|
|
225
|
+
title: str | None = None,
|
|
226
|
+
size: int | None = None,
|
|
227
|
+
xmin: float | None = None,
|
|
228
|
+
xmax: float | None = None,
|
|
229
|
+
xlabel: str | None = None,
|
|
230
|
+
ylabel: str | None = None,
|
|
231
|
+
xunit: str | None = None,
|
|
232
|
+
yunit: str | None = None,
|
|
233
|
+
**kwargs: dict,
|
|
234
|
+
) -> NewSignalParam:
|
|
235
|
+
"""Create parameters for a given signal type.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
stype: signal type
|
|
239
|
+
title: signal title
|
|
240
|
+
size: signal size (number of points)
|
|
241
|
+
xmin: minimum x value
|
|
242
|
+
xmax: maximum x value
|
|
243
|
+
xlabel: x axis label
|
|
244
|
+
ylabel: y axis label
|
|
245
|
+
xunit: x axis unit
|
|
246
|
+
yunit: y axis unit
|
|
247
|
+
**kwargs: additional parameters (specific to the signal type)
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Parameters object for the given signal type
|
|
251
|
+
"""
|
|
252
|
+
pclass = __get_signal_parameters_class(stype)
|
|
253
|
+
p = pclass.create(**kwargs)
|
|
254
|
+
if title is not None:
|
|
255
|
+
p.title = title
|
|
256
|
+
if size is not None:
|
|
257
|
+
p.size = size
|
|
258
|
+
if xmin is not None:
|
|
259
|
+
p.xmin = xmin
|
|
260
|
+
if xmax is not None:
|
|
261
|
+
p.xmax = xmax
|
|
262
|
+
if xlabel is not None:
|
|
263
|
+
p.xlabel = xlabel
|
|
264
|
+
if ylabel is not None:
|
|
265
|
+
p.ylabel = ylabel
|
|
266
|
+
if xunit is not None:
|
|
267
|
+
p.xunit = xunit
|
|
268
|
+
if yunit is not None:
|
|
269
|
+
p.yunit = yunit
|
|
270
|
+
return p
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class ZeroParam(NewSignalParam, title=_("Zero")):
|
|
274
|
+
"""Parameters for zero signal."""
|
|
275
|
+
|
|
276
|
+
def generate_1d_data(self) -> tuple[np.ndarray, np.ndarray]:
|
|
277
|
+
"""Compute 1D data based on current parameters.
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
Tuple of (x, y) arrays.
|
|
281
|
+
"""
|
|
282
|
+
x = self.generate_x_data()
|
|
283
|
+
return x, np.zeros_like(x)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
register_signal_parameters_class(SignalTypes.ZERO, ZeroParam)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class UniformDistribution1DParam(
|
|
290
|
+
NewSignalParam, base.UniformDistributionParam, title=_("Uniform distribution")
|
|
291
|
+
):
|
|
292
|
+
"""Uniform-distribution signal parameters."""
|
|
293
|
+
|
|
294
|
+
def generate_1d_data(self) -> tuple[np.ndarray, np.ndarray]:
|
|
295
|
+
"""Compute 1D data based on current parameters.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Tuple of (x, y) arrays.
|
|
299
|
+
"""
|
|
300
|
+
x = self.generate_x_data()
|
|
301
|
+
rng = np.random.default_rng(self.seed)
|
|
302
|
+
assert self.vmin is not None
|
|
303
|
+
assert self.vmax is not None
|
|
304
|
+
y = self.vmin + rng.random(len(x)) * (self.vmax - self.vmin)
|
|
305
|
+
return x, y
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
register_signal_parameters_class(
|
|
309
|
+
SignalTypes.UNIFORM_DISTRIBUTION, UniformDistribution1DParam
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
class NormalDistribution1DParam(
|
|
314
|
+
NewSignalParam, base.NormalDistributionParam, title=_("Normal distribution")
|
|
315
|
+
):
|
|
316
|
+
"""Normal-distribution signal parameters."""
|
|
317
|
+
|
|
318
|
+
def generate_1d_data(self) -> tuple[np.ndarray, np.ndarray]:
|
|
319
|
+
"""Compute 1D data based on current parameters.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
Tuple of (x, y) arrays.
|
|
323
|
+
"""
|
|
324
|
+
x = self.generate_x_data()
|
|
325
|
+
rng = np.random.default_rng(self.seed)
|
|
326
|
+
assert self.mu is not None
|
|
327
|
+
assert self.sigma is not None
|
|
328
|
+
y = rng.normal(self.mu, self.sigma, len(x))
|
|
329
|
+
return x, y
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
register_signal_parameters_class(
|
|
333
|
+
SignalTypes.NORMAL_DISTRIBUTION, NormalDistribution1DParam
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class PoissonDistribution1DParam(
|
|
338
|
+
NewSignalParam, base.PoissonDistributionParam, title=_("Poisson distribution")
|
|
339
|
+
):
|
|
340
|
+
"""Poisson-distribution signal parameters."""
|
|
341
|
+
|
|
342
|
+
def generate_1d_data(self) -> tuple[np.ndarray, np.ndarray]:
|
|
343
|
+
"""Compute 1D data based on current parameters.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
Tuple of (x, y) arrays.
|
|
347
|
+
"""
|
|
348
|
+
x = self.generate_x_data()
|
|
349
|
+
rng = np.random.default_rng(self.seed)
|
|
350
|
+
assert self.lam is not None
|
|
351
|
+
y = rng.poisson(lam=self.lam, size=len(x))
|
|
352
|
+
return x, y
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
register_signal_parameters_class(
|
|
356
|
+
SignalTypes.POISSON_DISTRIBUTION, PoissonDistribution1DParam
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
class BaseGaussLorentzVoigtParam(NewSignalParam):
|
|
361
|
+
"""Base parameters for Gaussian, Lorentzian and Voigt functions"""
|
|
362
|
+
|
|
363
|
+
STYPE: Type[SignalTypes] | None = None
|
|
364
|
+
|
|
365
|
+
a = gds.FloatItem("A", default=1.0)
|
|
366
|
+
y0 = gds.FloatItem("y<sub>0</sub>", default=0.0).set_pos(col=1)
|
|
367
|
+
sigma = gds.FloatItem("σ", default=1.0)
|
|
368
|
+
mu = gds.FloatItem("μ", default=0.0).set_pos(col=1)
|
|
369
|
+
|
|
370
|
+
def generate_title(self) -> str:
|
|
371
|
+
"""Generate a title based on current parameters."""
|
|
372
|
+
assert isinstance(self.STYPE, SignalTypes)
|
|
373
|
+
return (
|
|
374
|
+
f"{self.STYPE.name.lower()}(A={self.a:.3g},σ={self.sigma:.3g},"
|
|
375
|
+
f"μ={self.mu:.3g},y0={self.y0:.3g})"
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
def generate_1d_data(self) -> tuple[np.ndarray, np.ndarray]:
|
|
379
|
+
"""Compute 1D data based on current parameters.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
Tuple of (x, y) arrays
|
|
383
|
+
"""
|
|
384
|
+
x = self.generate_x_data()
|
|
385
|
+
func = {
|
|
386
|
+
SignalTypes.GAUSS: GaussianModel.func,
|
|
387
|
+
SignalTypes.LORENTZ: LorentzianModel.func,
|
|
388
|
+
SignalTypes.VOIGT: VoigtModel.func,
|
|
389
|
+
}[self.STYPE]
|
|
390
|
+
y = func(x, self.a, self.sigma, self.mu, self.y0)
|
|
391
|
+
return x, y
|
|
392
|
+
|
|
393
|
+
def get_expected_features(
|
|
394
|
+
self, start_ratio: float = 0.1, stop_ratio: float = 0.9
|
|
395
|
+
) -> ExpectedFeatures:
|
|
396
|
+
"""Calculate expected pulse features for this signal.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
start_ratio: Start ratio for rise time calculation
|
|
400
|
+
stop_ratio: Stop ratio for rise time calculation
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
ExpectedFeatures dataclass with all expected values
|
|
404
|
+
"""
|
|
405
|
+
if self.a is None or self.sigma is None:
|
|
406
|
+
raise ValueError("Parameters 'a' and 'sigma' must be set")
|
|
407
|
+
if self.a == 0 or self.sigma <= 0:
|
|
408
|
+
raise ValueError("Parameter 'a' must be non-zero and 'sigma' positive")
|
|
409
|
+
|
|
410
|
+
polarity = 1 if self.a > 0 else -1
|
|
411
|
+
|
|
412
|
+
# For Gaussian: peak amplitude is a / (sigma * sqrt(2*pi))
|
|
413
|
+
# This gives the actual maximum value of the Gaussian function
|
|
414
|
+
amplitude = abs(self.a) / (self.sigma * np.sqrt(2 * np.pi))
|
|
415
|
+
|
|
416
|
+
if self.STYPE == SignalTypes.GAUSS:
|
|
417
|
+
# Gaussian rise time: t_r = 2.563 * sigma (10% to 90%)
|
|
418
|
+
rise_time = 2.563 * self.sigma
|
|
419
|
+
elif self.STYPE == SignalTypes.LORENTZ:
|
|
420
|
+
# Lorentzian rise time: 2*sigma*sqrt(1/start_ratio - 1/stop_ratio)
|
|
421
|
+
rise_time = 2 * self.sigma * np.sqrt(1 / start_ratio - 1 / stop_ratio)
|
|
422
|
+
elif self.STYPE == SignalTypes.VOIGT:
|
|
423
|
+
# Voigt rise time: approximate as Gaussian for simplicity
|
|
424
|
+
rise_time = 2.563 * self.sigma
|
|
425
|
+
else:
|
|
426
|
+
raise ValueError(f"Unsupported signal type: {self.STYPE}")
|
|
427
|
+
|
|
428
|
+
# For Gaussian signals centered at mu
|
|
429
|
+
x_center = self.mu if self.mu is not None else 0.0
|
|
430
|
+
|
|
431
|
+
# Gaussian-specific calculations
|
|
432
|
+
if self.STYPE == SignalTypes.GAUSS:
|
|
433
|
+
# Time at 50% amplitude (FWHM calculation)
|
|
434
|
+
fwhm = 2.355 * self.sigma # Full Width at Half Maximum for Gaussian
|
|
435
|
+
# x50 is the 50% crossing on the rise (left side of peak)
|
|
436
|
+
x50 = x_center - self.sigma * np.sqrt(-2 * np.log(0.5)) # ~0.833σ
|
|
437
|
+
|
|
438
|
+
# Rise time from left 20% to left 80% (one-sided)
|
|
439
|
+
# For amplitude ratios: x = mu ± sigma * sqrt(-2 * ln(ratio))
|
|
440
|
+
t_20_left = x_center - self.sigma * np.sqrt(-2 * np.log(0.2)) # ~1.794σ
|
|
441
|
+
t_80_left = x_center - self.sigma * np.sqrt(-2 * np.log(0.8)) # ~0.668σ
|
|
442
|
+
actual_rise_time = abs(t_80_left - t_20_left)
|
|
443
|
+
|
|
444
|
+
# Fall time (symmetric for Gaussian)
|
|
445
|
+
fall_time = actual_rise_time
|
|
446
|
+
|
|
447
|
+
# Foot duration: For Gaussian, use approximation based on sigma
|
|
448
|
+
# Since Gaussian has no true flat foot, this is an approximation
|
|
449
|
+
foot_duration = 1.5 * self.sigma # Empirically derived approximation
|
|
450
|
+
|
|
451
|
+
else:
|
|
452
|
+
# For Lorentzian and Voigt, use approximations
|
|
453
|
+
x50 = x_center
|
|
454
|
+
actual_rise_time = rise_time # Use calculated rise_time
|
|
455
|
+
fall_time = rise_time
|
|
456
|
+
if self.STYPE == SignalTypes.LORENTZ:
|
|
457
|
+
fwhm = 2 * self.sigma
|
|
458
|
+
else:
|
|
459
|
+
fwhm = 2.355 * self.sigma
|
|
460
|
+
foot_duration = 2 * self.sigma # Approximation
|
|
461
|
+
|
|
462
|
+
return ExpectedFeatures(
|
|
463
|
+
signal_shape=SignalShape.SQUARE,
|
|
464
|
+
polarity=polarity,
|
|
465
|
+
amplitude=amplitude,
|
|
466
|
+
rise_time=actual_rise_time,
|
|
467
|
+
offset=self.y0 if self.y0 is not None else 0.0,
|
|
468
|
+
x50=x50,
|
|
469
|
+
x100=x_center, # Maximum is at center for Gaussian
|
|
470
|
+
foot_duration=foot_duration,
|
|
471
|
+
fall_time=fall_time,
|
|
472
|
+
fwhm=fwhm,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
def get_feature_tolerances(self) -> FeatureTolerances:
|
|
476
|
+
"""Get absolute tolerance values for pulse feature validation.
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
FeatureTolerances dataclass with adjusted tolerances for Gaussian signals
|
|
480
|
+
"""
|
|
481
|
+
# Gaussian signals may need slightly more relaxed tolerances due to smoothness
|
|
482
|
+
return FeatureTolerances(
|
|
483
|
+
rise_time=0.3, # Slightly higher tolerance for Gaussian rise time
|
|
484
|
+
fall_time=0.3, # Match rise time tolerance
|
|
485
|
+
x100=0.1, # Tighter tolerance for maximum position (should be exact)
|
|
486
|
+
fwhm=0.2, # Reasonable tolerance for FWHM
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
def get_crossing_time(self, edge: Literal["rise", "fall"], ratio: float) -> float:
|
|
490
|
+
"""Get the theoretical crossing time for the specified edge and ratio.
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
edge: Which edge to calculate ("rise" or "fall")
|
|
494
|
+
ratio: Crossing ratio (0.0 to 1.0)
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
Theoretical crossing time for the specified edge and ratio
|
|
498
|
+
"""
|
|
499
|
+
if self.a is None or self.sigma is None or self.mu is None:
|
|
500
|
+
raise ValueError("Parameters 'a', 'sigma', and 'mu' must be set")
|
|
501
|
+
if self.a == 0 or self.sigma <= 0:
|
|
502
|
+
raise ValueError("Parameter 'a' must be non-zero and 'sigma' positive")
|
|
503
|
+
if not 0.0 < ratio < 1.0:
|
|
504
|
+
raise ValueError("Ratio must be between 0.0 and 1.0")
|
|
505
|
+
|
|
506
|
+
if self.STYPE != SignalTypes.GAUSS:
|
|
507
|
+
raise NotImplementedError(
|
|
508
|
+
"Crossing time calculation is only implemented for Gaussian signals"
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
# For Gaussian: x = mu ± sigma * sqrt(-2 * ln(ratio))
|
|
512
|
+
delta_x = self.sigma * np.sqrt(-2 * np.log(ratio))
|
|
513
|
+
if edge == "rise":
|
|
514
|
+
return self.mu - delta_x
|
|
515
|
+
if edge == "fall":
|
|
516
|
+
return self.mu + delta_x
|
|
517
|
+
raise ValueError("Edge must be 'rise' or 'fall'")
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
class GaussParam(
|
|
521
|
+
BaseGaussLorentzVoigtParam,
|
|
522
|
+
title=_("Gaussian"),
|
|
523
|
+
comment="y = y<sub>0</sub> + "
|
|
524
|
+
"A/(σ √(2π)) exp(-((x - μ)<sup>2</sup>) / (2 σ<sup>2</sup>))",
|
|
525
|
+
):
|
|
526
|
+
"""Parameters for Gaussian function."""
|
|
527
|
+
|
|
528
|
+
STYPE = SignalTypes.GAUSS
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
register_signal_parameters_class(SignalTypes.GAUSS, GaussParam)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
class LorentzParam(
|
|
535
|
+
BaseGaussLorentzVoigtParam,
|
|
536
|
+
title=_("Lorentzian"),
|
|
537
|
+
comment="y = y<sub>0</sub> + A/(π σ (1 + ((x - μ)/σ)<sup>2</sup>))",
|
|
538
|
+
):
|
|
539
|
+
"""Parameters for Lorentzian function."""
|
|
540
|
+
|
|
541
|
+
STYPE = SignalTypes.LORENTZ
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
register_signal_parameters_class(SignalTypes.LORENTZ, LorentzParam)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
class VoigtParam(
|
|
548
|
+
BaseGaussLorentzVoigtParam,
|
|
549
|
+
title=_("Voigt"),
|
|
550
|
+
comment="y = y<sub>0</sub> + "
|
|
551
|
+
"A Re[exp(-z<sup>2</sup>) erfc(-j z)] / (σ √(2π)), "
|
|
552
|
+
"with z = (x - μ - j σ) / (σ √2)",
|
|
553
|
+
):
|
|
554
|
+
"""Parameters for Voigt function."""
|
|
555
|
+
|
|
556
|
+
STYPE = SignalTypes.VOIGT
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
register_signal_parameters_class(SignalTypes.VOIGT, VoigtParam)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
class PlanckParam(
|
|
563
|
+
NewSignalParam,
|
|
564
|
+
title=_("Blackbody (Planck)"),
|
|
565
|
+
comment="y = (2 h c<sup>2</sup>) / "
|
|
566
|
+
"(λ<sup>5</sup> (exp(h c / (λ k<sub>B</sub> T)) - 1))",
|
|
567
|
+
):
|
|
568
|
+
"""Planck radiation law."""
|
|
569
|
+
|
|
570
|
+
xmin = gds.FloatItem(
|
|
571
|
+
"λ<sub>min</sub>", default=1e-7, unit="m", min=0.0, nonzero=True
|
|
572
|
+
)
|
|
573
|
+
xmax = gds.FloatItem(
|
|
574
|
+
"λ<sub>max</sub>", default=1e-4, unit="m", min=0.0, nonzero=True
|
|
575
|
+
).set_prop("display", col=1)
|
|
576
|
+
T = gds.FloatItem(
|
|
577
|
+
"T", default=293.0, unit="K", min=0.0, nonzero=True, help=_("Temperature")
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
def generate_title(self) -> str:
|
|
581
|
+
"""Generate a title based on current parameters.
|
|
582
|
+
|
|
583
|
+
Returns:
|
|
584
|
+
Title string.
|
|
585
|
+
"""
|
|
586
|
+
return f"planck(T={self.T:.3g}K)"
|
|
587
|
+
|
|
588
|
+
@classmethod
|
|
589
|
+
def func(cls, wavelength: np.ndarray, temperature: float) -> np.ndarray:
|
|
590
|
+
"""Compute the Planck function.
|
|
591
|
+
|
|
592
|
+
Args:
|
|
593
|
+
wavelength: Wavelength (m).
|
|
594
|
+
T: Temperature (K).
|
|
595
|
+
|
|
596
|
+
Returns:
|
|
597
|
+
Spectral radiance (W m<sup>-2</sup> sr<sup>-1</sup> Hz<sup>-1</sup>).
|
|
598
|
+
"""
|
|
599
|
+
h = scipy.constants.h # Planck constant (J·s)
|
|
600
|
+
c = scipy.constants.c # Speed of light (m/s)
|
|
601
|
+
k = scipy.constants.k # Boltzmann constant (J/K)
|
|
602
|
+
c1 = 2 * h * c**2
|
|
603
|
+
c2 = (h * c) / k
|
|
604
|
+
denom = np.exp(c2 / (wavelength * temperature)) - 1.0
|
|
605
|
+
spectral_radiance = c1 / (wavelength**5 * (denom))
|
|
606
|
+
return spectral_radiance
|
|
607
|
+
|
|
608
|
+
def generate_1d_data(self) -> tuple[np.ndarray, np.ndarray]:
|
|
609
|
+
"""Compute 1D data based on current parameters.
|
|
610
|
+
|
|
611
|
+
Returns:
|
|
612
|
+
Tuple of (wavelength, spectral radiance) arrays.
|
|
613
|
+
"""
|
|
614
|
+
wavelength = self.generate_x_data()
|
|
615
|
+
assert self.T is not None
|
|
616
|
+
y = self.func(wavelength, self.T)
|
|
617
|
+
return wavelength, y
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
register_signal_parameters_class(SignalTypes.PLANCK, PlanckParam)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
class FreqUnits(enum.Enum):
|
|
624
|
+
"""Frequency units"""
|
|
625
|
+
|
|
626
|
+
HZ = "Hz"
|
|
627
|
+
KHZ = "kHz"
|
|
628
|
+
MHZ = "MHz"
|
|
629
|
+
GHZ = "GHz"
|
|
630
|
+
|
|
631
|
+
@classmethod
|
|
632
|
+
def convert_in_hz(cls, value, unit):
|
|
633
|
+
"""Convert value in Hz"""
|
|
634
|
+
factor = {cls.HZ: 1, cls.KHZ: 1e3, cls.MHZ: 1e6, cls.GHZ: 1e9}.get(unit)
|
|
635
|
+
if factor is None:
|
|
636
|
+
raise ValueError(f"Unknown unit: {unit}")
|
|
637
|
+
return value * factor
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
class BasePeriodicParam(NewSignalParam):
|
|
641
|
+
"""Parameters for periodic functions"""
|
|
642
|
+
|
|
643
|
+
STYPE: Type[SignalTypes] | None = None
|
|
644
|
+
|
|
645
|
+
def get_frequency_in_hz(self):
|
|
646
|
+
"""Return frequency in Hz"""
|
|
647
|
+
return FreqUnits.convert_in_hz(self.freq, self.freq_unit)
|
|
648
|
+
|
|
649
|
+
# Redefining some parameters with more appropriate defaults
|
|
650
|
+
xunit = gds.StringItem(_("X unit"), default="s")
|
|
651
|
+
|
|
652
|
+
a = gds.FloatItem("A", default=1.0)
|
|
653
|
+
offset = gds.FloatItem("y<sub>0</sub>", default=0.0).set_pos(col=1)
|
|
654
|
+
freq = gds.FloatItem("f", default=1.0)
|
|
655
|
+
freq_unit = gds.ChoiceItem(_("Unit"), FreqUnits, default=FreqUnits.HZ).set_pos(
|
|
656
|
+
col=1
|
|
657
|
+
)
|
|
658
|
+
phase = gds.FloatItem("φ", default=0.0, unit="°")
|
|
659
|
+
|
|
660
|
+
def generate_title(self) -> str:
|
|
661
|
+
"""Generate a title based on current parameters."""
|
|
662
|
+
assert isinstance(self.STYPE, SignalTypes)
|
|
663
|
+
freq_hz = self.get_frequency_in_hz()
|
|
664
|
+
title = (
|
|
665
|
+
f"{self.STYPE.name.lower()}(f={freq_hz:.3g}Hz,"
|
|
666
|
+
f"A={self.a:.3g},y0={self.offset:.3g},φ={self.phase:.3g}°)"
|
|
667
|
+
)
|
|
668
|
+
return title
|
|
669
|
+
|
|
670
|
+
def generate_1d_data(self) -> tuple[np.ndarray, np.ndarray]:
|
|
671
|
+
"""Compute 1D data based on current parameters.
|
|
672
|
+
|
|
673
|
+
Returns:
|
|
674
|
+
Tuple of (x, y) arrays
|
|
675
|
+
"""
|
|
676
|
+
x = self.generate_x_data()
|
|
677
|
+
func = {
|
|
678
|
+
SignalTypes.SINE: np.sin,
|
|
679
|
+
SignalTypes.COSINE: np.cos,
|
|
680
|
+
SignalTypes.SAWTOOTH: sps.sawtooth,
|
|
681
|
+
SignalTypes.TRIANGLE: triangle_func,
|
|
682
|
+
SignalTypes.SQUARE: sps.square,
|
|
683
|
+
SignalTypes.SINC: np.sinc,
|
|
684
|
+
}[self.STYPE]
|
|
685
|
+
freq = self.get_frequency_in_hz()
|
|
686
|
+
y = self.a * func(2 * np.pi * freq * x + np.deg2rad(self.phase)) + self.offset
|
|
687
|
+
return x, y
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
class SineParam(
|
|
691
|
+
BasePeriodicParam, title=_("Sine"), comment="y = y<sub>0</sub> + A sin(2π f x + φ)"
|
|
692
|
+
):
|
|
693
|
+
"""Parameters for sine function."""
|
|
694
|
+
|
|
695
|
+
STYPE = SignalTypes.SINE
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
register_signal_parameters_class(SignalTypes.SINE, SineParam)
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
class CosineParam(
|
|
702
|
+
BasePeriodicParam,
|
|
703
|
+
title=_("Cosine"),
|
|
704
|
+
comment="y = y<sub>0</sub> + A cos(2π f x + φ)",
|
|
705
|
+
):
|
|
706
|
+
"""Parameters for cosine function."""
|
|
707
|
+
|
|
708
|
+
STYPE = SignalTypes.COSINE
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
register_signal_parameters_class(SignalTypes.COSINE, CosineParam)
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
class SawtoothParam(
|
|
715
|
+
BasePeriodicParam,
|
|
716
|
+
title=_("Sawtooth"),
|
|
717
|
+
comment="y = y<sub>0</sub> + A (2 (f x + φ/(2π) - |f x + φ/(2π) + 1/2|))",
|
|
718
|
+
):
|
|
719
|
+
"""Parameters for sawtooth function."""
|
|
720
|
+
|
|
721
|
+
STYPE = SignalTypes.SAWTOOTH
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
register_signal_parameters_class(SignalTypes.SAWTOOTH, SawtoothParam)
|
|
725
|
+
|
|
726
|
+
|
|
727
|
+
class TriangleParam(
|
|
728
|
+
BasePeriodicParam,
|
|
729
|
+
title=_("Triangle"),
|
|
730
|
+
comment="y = y<sub>0</sub> + A sawtooth(2π f x + φ, width=0.5)",
|
|
731
|
+
):
|
|
732
|
+
"""Parameters for triangle function."""
|
|
733
|
+
|
|
734
|
+
STYPE = SignalTypes.TRIANGLE
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
register_signal_parameters_class(SignalTypes.TRIANGLE, TriangleParam)
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
class SquareParam(
|
|
741
|
+
BasePeriodicParam,
|
|
742
|
+
title=_("Square"),
|
|
743
|
+
comment="y = y<sub>0</sub> + A sgn(sin(2π f x + φ))",
|
|
744
|
+
):
|
|
745
|
+
"""Parameters for square function."""
|
|
746
|
+
|
|
747
|
+
STYPE = SignalTypes.SQUARE
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
register_signal_parameters_class(SignalTypes.SQUARE, SquareParam)
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
class SincParam(
|
|
754
|
+
BasePeriodicParam,
|
|
755
|
+
title=_("Cardinal sine"),
|
|
756
|
+
comment="y = y<sub>0</sub> + A sinc(f x + φ)",
|
|
757
|
+
):
|
|
758
|
+
"""Parameters for cardinal sine function."""
|
|
759
|
+
|
|
760
|
+
STYPE = SignalTypes.SINC
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
register_signal_parameters_class(SignalTypes.SINC, SincParam)
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
class LinearChirpParam(
|
|
767
|
+
NewSignalParam,
|
|
768
|
+
title=_("Linear chirp"),
|
|
769
|
+
comment="y = y<sub>0</sub> + a sin(φ<sub>0</sub> "
|
|
770
|
+
"+ 2π (f<sub>0</sub> x + 0.5 k x²))",
|
|
771
|
+
):
|
|
772
|
+
"""Linear chirp function."""
|
|
773
|
+
|
|
774
|
+
a = gds.FloatItem("A", default=1.0, help=_("Amplitude"))
|
|
775
|
+
phi0 = gds.FloatItem(
|
|
776
|
+
"φ<sub>0</sub>", default=0.0, help=_("Initial phase")
|
|
777
|
+
).set_prop("display", col=1)
|
|
778
|
+
k = gds.FloatItem("k", default=1.0, help=_("Chirp rate (f<sup>-2</sup>)"))
|
|
779
|
+
offset = gds.FloatItem(
|
|
780
|
+
"y<sub>0</sub>", default=0.0, help=_("Vertical offset")
|
|
781
|
+
).set_prop("display", col=1)
|
|
782
|
+
f0 = gds.FloatItem("f<sub>0</sub>", default=1.0, help=_("Initial frequency (Hz)"))
|
|
783
|
+
|
|
784
|
+
def generate_title(self) -> str:
|
|
785
|
+
"""Generate a title based on current parameters.
|
|
786
|
+
|
|
787
|
+
Returns:
|
|
788
|
+
Title string.
|
|
789
|
+
"""
|
|
790
|
+
return (
|
|
791
|
+
f"chirp(A={self.a:.3g},"
|
|
792
|
+
f"k={self.k:.3g},"
|
|
793
|
+
f"f0={self.f0:.3g},"
|
|
794
|
+
f"φ0={self.phi0:.3g},"
|
|
795
|
+
f"y0={self.offset:.3g})"
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
@classmethod
|
|
799
|
+
def func(
|
|
800
|
+
cls, x: np.ndarray, a: float, k: float, f0: float, phi0: float, offset: float
|
|
801
|
+
) -> np.ndarray:
|
|
802
|
+
"""Compute the linear chirp function.
|
|
803
|
+
|
|
804
|
+
Args:
|
|
805
|
+
x: X data array.
|
|
806
|
+
a: Amplitude.
|
|
807
|
+
k: Chirp rate (s<sup>-2</sup>).
|
|
808
|
+
f0: Initial frequency (Hz).
|
|
809
|
+
phi0: Initial phase.
|
|
810
|
+
offset: Vertical offset.
|
|
811
|
+
|
|
812
|
+
Returns:
|
|
813
|
+
Y data array computed using the chirp function.
|
|
814
|
+
"""
|
|
815
|
+
phase = phi0 + 2 * np.pi * (f0 * x + 0.5 * k * x**2)
|
|
816
|
+
return offset + a * np.sin(phase)
|
|
817
|
+
|
|
818
|
+
def generate_1d_data(self) -> tuple[np.ndarray, np.ndarray]:
|
|
819
|
+
"""Compute 1D data based on current parameters.
|
|
820
|
+
|
|
821
|
+
Returns:
|
|
822
|
+
Tuple of (x, y) arrays.
|
|
823
|
+
"""
|
|
824
|
+
assert self.a is not None
|
|
825
|
+
assert self.k is not None
|
|
826
|
+
assert self.f0 is not None
|
|
827
|
+
assert self.phi0 is not None
|
|
828
|
+
assert self.offset is not None
|
|
829
|
+
x = self.generate_x_data()
|
|
830
|
+
y = self.func(x, self.a, self.k, self.f0, self.phi0, self.offset)
|
|
831
|
+
return x, y
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
register_signal_parameters_class(SignalTypes.LINEARCHIRP, LinearChirpParam)
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
class StepParam(NewSignalParam, title=_("Step")):
|
|
838
|
+
"""Parameters for step function."""
|
|
839
|
+
|
|
840
|
+
a1 = gds.FloatItem("A<sub>1</sub>", default=0.0)
|
|
841
|
+
a2 = gds.FloatItem("A<sub>2</sub>", default=1.0).set_pos(col=1)
|
|
842
|
+
x0 = gds.FloatItem("x<sub>0</sub>", default=0.0)
|
|
843
|
+
|
|
844
|
+
def generate_title(self) -> str:
|
|
845
|
+
"""Generate a title based on current parameters."""
|
|
846
|
+
return f"step(a1={self.a1:.3g},a2={self.a2:.3g},x0={self.x0:.3g})"
|
|
847
|
+
|
|
848
|
+
def generate_1d_data(self) -> tuple[np.ndarray, np.ndarray]:
|
|
849
|
+
"""Compute 1D data based on current parameters.
|
|
850
|
+
|
|
851
|
+
Returns:
|
|
852
|
+
Tuple of (x, y) arrays
|
|
853
|
+
"""
|
|
854
|
+
x = self.generate_x_data()
|
|
855
|
+
y = np.ones_like(x) * self.a1
|
|
856
|
+
y[x > self.x0] = self.a2
|
|
857
|
+
return x, y
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
register_signal_parameters_class(SignalTypes.STEP, StepParam)
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
class ExponentialParam(
|
|
864
|
+
NewSignalParam, title=_("Exponential"), comment="y = A exp(B x) + y<sub>0</sub>"
|
|
865
|
+
):
|
|
866
|
+
"""Parameters for exponential function."""
|
|
867
|
+
|
|
868
|
+
a = gds.FloatItem("A", default=1.0)
|
|
869
|
+
offset = gds.FloatItem("y<sub>0</sub>", default=0.0)
|
|
870
|
+
exponent = gds.FloatItem("B", default=1.0)
|
|
871
|
+
|
|
872
|
+
def generate_title(self) -> str:
|
|
873
|
+
"""Generate a title based on current parameters."""
|
|
874
|
+
return f"exponential(A={self.a:.3g},B={self.exponent:.3g},y0={self.offset:.3g})"
|
|
875
|
+
|
|
876
|
+
def generate_1d_data(self) -> tuple[np.ndarray, np.ndarray]:
|
|
877
|
+
"""Compute 1D data based on current parameters.
|
|
878
|
+
|
|
879
|
+
Returns:
|
|
880
|
+
Tuple of (x, y) arrays
|
|
881
|
+
"""
|
|
882
|
+
x = self.generate_x_data()
|
|
883
|
+
y = self.a * np.exp(self.exponent * x) + self.offset
|
|
884
|
+
return x, y
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
register_signal_parameters_class(SignalTypes.EXPONENTIAL, ExponentialParam)
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
class LogisticParam(
|
|
891
|
+
NewSignalParam,
|
|
892
|
+
title=_("Logistic"),
|
|
893
|
+
comment="y = y<sub>0</sub> + A / (1 + exp(-k (x - x<sub>0</sub>)))",
|
|
894
|
+
):
|
|
895
|
+
"""Logistic function."""
|
|
896
|
+
|
|
897
|
+
a = gds.FloatItem("A", default=1.0, help=_("Amplitude"))
|
|
898
|
+
x0 = gds.FloatItem(
|
|
899
|
+
"x<sub>0</sub>", default=0.0, help=_("Horizontal offset")
|
|
900
|
+
).set_prop("display", col=1)
|
|
901
|
+
k = gds.FloatItem("k", default=1.0, help=_("Growth or decay rate"))
|
|
902
|
+
offset = gds.FloatItem(
|
|
903
|
+
"y<sub>0</sub>", default=0.0, help=_("Vertical offset")
|
|
904
|
+
).set_prop("display", col=1)
|
|
905
|
+
|
|
906
|
+
def generate_title(self) -> str:
|
|
907
|
+
"""Generate a title based on current parameters.
|
|
908
|
+
|
|
909
|
+
Returns:
|
|
910
|
+
Title string.
|
|
911
|
+
"""
|
|
912
|
+
return (
|
|
913
|
+
f"logistic(A={self.a:.3g},"
|
|
914
|
+
f"k={self.k:.3g},"
|
|
915
|
+
f"x0={self.x0:.3g},"
|
|
916
|
+
f"y0={self.offset:.3g})"
|
|
917
|
+
)
|
|
918
|
+
|
|
919
|
+
@classmethod
|
|
920
|
+
def func(
|
|
921
|
+
cls, x: np.ndarray, a: float, k: float, x0: float, offset: float
|
|
922
|
+
) -> np.ndarray:
|
|
923
|
+
"""Compute the logistic function.
|
|
924
|
+
|
|
925
|
+
Args:
|
|
926
|
+
x: X data array.
|
|
927
|
+
a: Amplitude.
|
|
928
|
+
k: Growth or decay rate.
|
|
929
|
+
x0: Horizontal offset.
|
|
930
|
+
offset: Vertical offset.
|
|
931
|
+
|
|
932
|
+
Returns:
|
|
933
|
+
Y data array computed using the logistic function.
|
|
934
|
+
"""
|
|
935
|
+
return offset + a / (1.0 + np.exp(-k * (x - x0)))
|
|
936
|
+
|
|
937
|
+
def generate_1d_data(self) -> tuple[np.ndarray, np.ndarray]:
|
|
938
|
+
"""Compute 1D data based on current parameters.
|
|
939
|
+
|
|
940
|
+
Returns:
|
|
941
|
+
Tuple of (x, y) arrays.
|
|
942
|
+
"""
|
|
943
|
+
assert self.a is not None
|
|
944
|
+
assert self.k is not None
|
|
945
|
+
assert self.x0 is not None
|
|
946
|
+
assert self.offset is not None
|
|
947
|
+
x = self.generate_x_data()
|
|
948
|
+
y = self.func(x, self.a, self.k, self.x0, self.offset)
|
|
949
|
+
return x, y
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
register_signal_parameters_class(SignalTypes.LOGISTIC, LogisticParam)
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
class PulseParam(NewSignalParam, title=_("Pulse")):
|
|
956
|
+
"""Parameters for pulse function."""
|
|
957
|
+
|
|
958
|
+
amp = gds.FloatItem("Amplitude", default=1.0)
|
|
959
|
+
start = gds.FloatItem(_("Start"), default=0.0).set_pos(col=1)
|
|
960
|
+
offset = gds.FloatItem(_("Offset"), default=10.0)
|
|
961
|
+
stop = gds.FloatItem(_("End"), default=5.0).set_pos(col=1)
|
|
962
|
+
|
|
963
|
+
def generate_title(self) -> str:
|
|
964
|
+
"""Generate a title based on current parameters."""
|
|
965
|
+
return (
|
|
966
|
+
f"pulse(start={self.start:.3g},stop={self.stop:.3g},"
|
|
967
|
+
f"offset={self.offset:.3g},amp={self.amp:.3g})"
|
|
968
|
+
)
|
|
969
|
+
|
|
970
|
+
def generate_1d_data(self) -> tuple[np.ndarray, np.ndarray]:
|
|
971
|
+
"""Compute 1D data based on current parameters.
|
|
972
|
+
|
|
973
|
+
Returns:
|
|
974
|
+
Tuple of (x, y) arrays
|
|
975
|
+
"""
|
|
976
|
+
x = self.generate_x_data()
|
|
977
|
+
y = np.full_like(x, self.offset)
|
|
978
|
+
y[(x >= self.start) & (x <= self.stop)] += self.amp
|
|
979
|
+
return x, y
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
register_signal_parameters_class(SignalTypes.PULSE, PulseParam)
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
@dataclass
|
|
986
|
+
class ExpectedFeatures:
|
|
987
|
+
"""Expected pulse feature values for validation."""
|
|
988
|
+
|
|
989
|
+
signal_shape: SignalShape
|
|
990
|
+
polarity: int
|
|
991
|
+
amplitude: float
|
|
992
|
+
rise_time: float # Rise time between specified ratios
|
|
993
|
+
offset: float
|
|
994
|
+
x50: float
|
|
995
|
+
x100: float # Time at 100% amplitude (maximum)
|
|
996
|
+
foot_duration: float
|
|
997
|
+
fall_time: float | None = None # Fall time between specified ratios
|
|
998
|
+
fwhm: float | None = None
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
@dataclass
|
|
1002
|
+
class FeatureTolerances:
|
|
1003
|
+
"""Absolute tolerance values for pulse feature validation."""
|
|
1004
|
+
|
|
1005
|
+
polarity: float = 1e-8
|
|
1006
|
+
amplitude: float = 0.5
|
|
1007
|
+
rise_time: float = 0.2
|
|
1008
|
+
offset: float = 0.5
|
|
1009
|
+
x50: float = 0.1
|
|
1010
|
+
x100: float = 0.6 # Tolerance for time at 100% amplitude
|
|
1011
|
+
foot_duration: float = 0.5
|
|
1012
|
+
fall_time: float = 1.0
|
|
1013
|
+
fwhm: float = 0.5
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
class BasePulseParam(NewSignalParam):
|
|
1017
|
+
"""Base class for pulse signal parameters."""
|
|
1018
|
+
|
|
1019
|
+
SEED = 0
|
|
1020
|
+
|
|
1021
|
+
# Redefine NewSignalParam parameters with more appropriate defaults
|
|
1022
|
+
xmin = gds.FloatItem(_("Start time"), default=0.0)
|
|
1023
|
+
xmax = gds.FloatItem(_("End time"), default=10.0)
|
|
1024
|
+
size = gds.IntItem(_("Number of points"), default=1000, min=1)
|
|
1025
|
+
|
|
1026
|
+
# Specific pulse parameters
|
|
1027
|
+
offset = gds.FloatItem(_("Initial value"), default=0.0)
|
|
1028
|
+
amplitude = gds.FloatItem(_("Amplitude"), default=5.0).set_pos(col=1)
|
|
1029
|
+
noise_amplitude = gds.FloatItem(_("Noise amplitude"), default=0.2, min=0.0)
|
|
1030
|
+
x_rise_start = gds.FloatItem(_("Rise start time"), default=3.0, min=0.0)
|
|
1031
|
+
total_rise_time = gds.FloatItem(_("Total rise time"), default=2.0, min=0.0).set_pos(
|
|
1032
|
+
col=1
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
def get_crossing_time(self, edge: Literal["rise", "fall"], ratio: float) -> float:
|
|
1036
|
+
"""Get the theoretical crossing time for the specified edge and ratio.
|
|
1037
|
+
|
|
1038
|
+
Args:
|
|
1039
|
+
edge: Which edge to calculate ("rise" or "fall")
|
|
1040
|
+
ratio: Crossing ratio (0.0 to 1.0)
|
|
1041
|
+
|
|
1042
|
+
Returns:
|
|
1043
|
+
Theoretical crossing time for the specified edge and ratio
|
|
1044
|
+
"""
|
|
1045
|
+
if edge == "rise":
|
|
1046
|
+
return self.x_rise_start + ratio * self.total_rise_time
|
|
1047
|
+
raise NotImplementedError(
|
|
1048
|
+
"Fall edge crossing time not implemented for this signal type"
|
|
1049
|
+
)
|
|
1050
|
+
|
|
1051
|
+
def get_expected_features(
|
|
1052
|
+
self, start_ratio: float = 0.1, stop_ratio: float = 0.9
|
|
1053
|
+
) -> ExpectedFeatures:
|
|
1054
|
+
"""Calculate expected pulse features for this signal.
|
|
1055
|
+
|
|
1056
|
+
Args:
|
|
1057
|
+
start_ratio: Start ratio for rise time calculation
|
|
1058
|
+
stop_ratio: Stop ratio for rise time calculation
|
|
1059
|
+
|
|
1060
|
+
Returns:
|
|
1061
|
+
ExpectedFeatures dataclass with all expected values
|
|
1062
|
+
"""
|
|
1063
|
+
y_end_value = self.offset + self.amplitude
|
|
1064
|
+
return ExpectedFeatures(
|
|
1065
|
+
signal_shape=SignalShape.STEP,
|
|
1066
|
+
polarity=1 if y_end_value > self.offset else -1,
|
|
1067
|
+
amplitude=abs(y_end_value - self.offset),
|
|
1068
|
+
rise_time=(stop_ratio - start_ratio) * self.total_rise_time,
|
|
1069
|
+
offset=self.offset,
|
|
1070
|
+
x50=self.x_rise_start + 0.5 * self.total_rise_time,
|
|
1071
|
+
x100=self.x_rise_start + self.total_rise_time,
|
|
1072
|
+
foot_duration=self.x_rise_start - self.xmin,
|
|
1073
|
+
)
|
|
1074
|
+
|
|
1075
|
+
def get_feature_tolerances(self) -> FeatureTolerances:
|
|
1076
|
+
"""Get absolute tolerance values for pulse feature validation.
|
|
1077
|
+
|
|
1078
|
+
Returns:
|
|
1079
|
+
FeatureTolerances dataclass with default tolerance values
|
|
1080
|
+
"""
|
|
1081
|
+
return FeatureTolerances()
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
class StepPulseParam(BasePulseParam, title=_("Step pulse with noise")):
|
|
1085
|
+
"""Parameters for generating step signals with configurable rise time."""
|
|
1086
|
+
|
|
1087
|
+
def generate_title(self) -> str:
|
|
1088
|
+
"""Generate a title based on current parameters."""
|
|
1089
|
+
return (
|
|
1090
|
+
f"step_pulse(rise_time={self.total_rise_time:.3g},"
|
|
1091
|
+
f"x_start={self.x_rise_start:.3g},offset={self.offset:.3g},"
|
|
1092
|
+
f"amp={self.amplitude:.3g})"
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
def generate_1d_data(self) -> tuple[np.ndarray, np.ndarray]:
|
|
1096
|
+
"""Generate a noisy step signal with a linear rise.
|
|
1097
|
+
|
|
1098
|
+
The function creates a time vector and generates a signal that starts at
|
|
1099
|
+
`offset`, rises linearly to `offset + amplitude` starting at `x_rise_start` over
|
|
1100
|
+
a duration of `total_rise_time`, and remains at the final value afterwards.
|
|
1101
|
+
Gaussian noise is added to the signal.
|
|
1102
|
+
|
|
1103
|
+
Returns:
|
|
1104
|
+
Tuple containing the time vector and noisy step signal.
|
|
1105
|
+
"""
|
|
1106
|
+
# time vector
|
|
1107
|
+
x = self.generate_x_data()
|
|
1108
|
+
|
|
1109
|
+
# Calculate final value from offset and amplitude
|
|
1110
|
+
y_final = self.offset + self.amplitude
|
|
1111
|
+
|
|
1112
|
+
# creating the signal
|
|
1113
|
+
rise_end_time = self.x_rise_start + self.total_rise_time
|
|
1114
|
+
y = np.piecewise(
|
|
1115
|
+
x,
|
|
1116
|
+
[
|
|
1117
|
+
x < self.x_rise_start,
|
|
1118
|
+
(x >= self.x_rise_start) & (x < rise_end_time),
|
|
1119
|
+
x >= rise_end_time,
|
|
1120
|
+
],
|
|
1121
|
+
[
|
|
1122
|
+
self.offset,
|
|
1123
|
+
lambda t: (
|
|
1124
|
+
self.offset
|
|
1125
|
+
+ (y_final - self.offset)
|
|
1126
|
+
* (t - self.x_rise_start)
|
|
1127
|
+
/ self.total_rise_time
|
|
1128
|
+
),
|
|
1129
|
+
y_final,
|
|
1130
|
+
],
|
|
1131
|
+
)
|
|
1132
|
+
rdg = np.random.default_rng(self.SEED)
|
|
1133
|
+
noise = rdg.normal(0, self.noise_amplitude, size=len(y))
|
|
1134
|
+
y_noisy = y + noise
|
|
1135
|
+
|
|
1136
|
+
return x, y_noisy
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
register_signal_parameters_class(SignalTypes.STEP_PULSE, StepPulseParam)
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
class SquarePulseParam(BasePulseParam, title=_("Square pulse with noise")):
|
|
1143
|
+
"""Parameters for generating square signals with configurable rise/fall times."""
|
|
1144
|
+
|
|
1145
|
+
# Redefine NewSignalParam parameters with more appropriate defaults
|
|
1146
|
+
xmax = gds.FloatItem(_("End time"), default=20.0)
|
|
1147
|
+
|
|
1148
|
+
# Specific square pulse parameters
|
|
1149
|
+
fwhm = gds.FloatItem(_("Full Width at Half Maximum"), default=5.5, min=0.0)
|
|
1150
|
+
total_fall_time = gds.FloatItem(_("Total fall time"), default=5.0, min=0.0).set_pos(
|
|
1151
|
+
col=1
|
|
1152
|
+
)
|
|
1153
|
+
|
|
1154
|
+
@property
|
|
1155
|
+
def square_duration(self) -> float:
|
|
1156
|
+
"""Calculate the square duration from FWHM and total rise/fall times."""
|
|
1157
|
+
return self.fwhm - 0.5 * self.total_rise_time - 0.5 * self.total_fall_time
|
|
1158
|
+
|
|
1159
|
+
def get_plateau_range(self) -> tuple[float, float]:
|
|
1160
|
+
"""Get the theoretical plateau range (start, end) for the square signal.
|
|
1161
|
+
|
|
1162
|
+
Returns:
|
|
1163
|
+
Tuple with (start, end) times of the plateau
|
|
1164
|
+
"""
|
|
1165
|
+
return (
|
|
1166
|
+
self.x_rise_start + self.total_rise_time,
|
|
1167
|
+
self.x_rise_start + self.total_rise_time + self.square_duration,
|
|
1168
|
+
)
|
|
1169
|
+
|
|
1170
|
+
def get_crossing_time(self, edge: Literal["rise", "fall"], ratio: float) -> float:
|
|
1171
|
+
"""Get the theoretical crossing time for the specified edge and ratio.
|
|
1172
|
+
|
|
1173
|
+
Args:
|
|
1174
|
+
edge: Which edge to calculate ("rise" or "fall")
|
|
1175
|
+
ratio: Crossing ratio (0.0 to 1.0)
|
|
1176
|
+
|
|
1177
|
+
Returns:
|
|
1178
|
+
Theoretical crossing time for the specified edge and ratio
|
|
1179
|
+
"""
|
|
1180
|
+
if edge == "rise":
|
|
1181
|
+
return super().get_crossing_time(edge, ratio)
|
|
1182
|
+
if edge == "fall":
|
|
1183
|
+
t_start_fall = (
|
|
1184
|
+
self.x_rise_start + self.total_rise_time + self.square_duration
|
|
1185
|
+
)
|
|
1186
|
+
return t_start_fall + ratio * self.total_fall_time
|
|
1187
|
+
raise ValueError("edge must be 'rise' or 'fall'")
|
|
1188
|
+
|
|
1189
|
+
def get_expected_features(
|
|
1190
|
+
self, start_ratio: float = 0.1, stop_ratio: float = 0.9
|
|
1191
|
+
) -> ExpectedFeatures:
|
|
1192
|
+
"""Calculate expected pulse features for this signal.
|
|
1193
|
+
|
|
1194
|
+
Args:
|
|
1195
|
+
start_ratio: Start ratio for rise time calculation
|
|
1196
|
+
stop_ratio: Stop ratio for rise time calculation
|
|
1197
|
+
|
|
1198
|
+
Returns:
|
|
1199
|
+
ExpectedFeatures dataclass with all expected values
|
|
1200
|
+
"""
|
|
1201
|
+
features = super().get_expected_features(start_ratio, stop_ratio)
|
|
1202
|
+
features.signal_shape = SignalShape.SQUARE
|
|
1203
|
+
features.fall_time = np.abs(stop_ratio - start_ratio) * self.total_fall_time
|
|
1204
|
+
features.fwhm = self.fwhm
|
|
1205
|
+
return features
|
|
1206
|
+
|
|
1207
|
+
def get_feature_tolerances(self) -> FeatureTolerances:
|
|
1208
|
+
"""Get absolute tolerance values for square signal feature validation.
|
|
1209
|
+
|
|
1210
|
+
Returns:
|
|
1211
|
+
FeatureTolerances dataclass with square-specific tolerance values
|
|
1212
|
+
"""
|
|
1213
|
+
return FeatureTolerances(
|
|
1214
|
+
x100=0.8, # Looser tolerance for square signals
|
|
1215
|
+
)
|
|
1216
|
+
|
|
1217
|
+
def generate_title(self) -> str:
|
|
1218
|
+
"""Generate a title based on current parameters."""
|
|
1219
|
+
return (
|
|
1220
|
+
f"square_pulse(rise_time={self.total_rise_time:.3g},"
|
|
1221
|
+
f"fall_time={self.total_fall_time:.3g},"
|
|
1222
|
+
f"fwhm={self.fwhm:.3g},offset={self.offset:.3g},"
|
|
1223
|
+
f"amp={self.amplitude:.3g})"
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1226
|
+
def generate_1d_data(self) -> tuple[np.ndarray, np.ndarray]:
|
|
1227
|
+
"""Generate a synthetic square-like signal with configurable parameters.
|
|
1228
|
+
|
|
1229
|
+
Generates a synthetic square-like signal with configurable rise, plateau,
|
|
1230
|
+
and fall times, and adds Gaussian noise.
|
|
1231
|
+
|
|
1232
|
+
Returns:
|
|
1233
|
+
Tuple containing the time vector and noisy square signal.
|
|
1234
|
+
"""
|
|
1235
|
+
# time vector
|
|
1236
|
+
x = self.generate_x_data()
|
|
1237
|
+
|
|
1238
|
+
# Calculate high value from offset and amplitude
|
|
1239
|
+
y_high = self.offset + self.amplitude
|
|
1240
|
+
|
|
1241
|
+
x_rise_end = self.x_rise_start + self.total_rise_time
|
|
1242
|
+
x_start_fall = self.x_rise_start + self.total_rise_time + self.square_duration
|
|
1243
|
+
# creating the signal
|
|
1244
|
+
y = np.piecewise(
|
|
1245
|
+
x,
|
|
1246
|
+
[
|
|
1247
|
+
x < self.x_rise_start,
|
|
1248
|
+
(x >= self.x_rise_start) & (x < x_rise_end),
|
|
1249
|
+
(x >= x_rise_end) & (x < x_start_fall),
|
|
1250
|
+
(x >= x_start_fall) & (x < x_start_fall + self.total_fall_time),
|
|
1251
|
+
x >= self.total_fall_time + x_start_fall,
|
|
1252
|
+
],
|
|
1253
|
+
[
|
|
1254
|
+
self.offset,
|
|
1255
|
+
lambda t: (
|
|
1256
|
+
self.offset
|
|
1257
|
+
+ (y_high - self.offset)
|
|
1258
|
+
* (t - self.x_rise_start)
|
|
1259
|
+
/ self.total_rise_time
|
|
1260
|
+
),
|
|
1261
|
+
y_high,
|
|
1262
|
+
lambda t: y_high
|
|
1263
|
+
- (y_high - self.offset) * (t - x_start_fall) / self.total_fall_time,
|
|
1264
|
+
self.offset,
|
|
1265
|
+
],
|
|
1266
|
+
)
|
|
1267
|
+
rdg = np.random.default_rng(self.SEED)
|
|
1268
|
+
noise = rdg.normal(0, self.noise_amplitude, size=len(y))
|
|
1269
|
+
y_noisy = y + noise
|
|
1270
|
+
|
|
1271
|
+
return x, y_noisy
|
|
1272
|
+
|
|
1273
|
+
|
|
1274
|
+
register_signal_parameters_class(SignalTypes.SQUARE_PULSE, SquarePulseParam)
|
|
1275
|
+
|
|
1276
|
+
|
|
1277
|
+
class PolyParam(NewSignalParam, title=_("Polynomial")):
|
|
1278
|
+
"""Parameters for polynomial function."""
|
|
1279
|
+
|
|
1280
|
+
a0 = gds.FloatItem("a0", default=1.0)
|
|
1281
|
+
a3 = gds.FloatItem("a3", default=0.0).set_pos(col=1)
|
|
1282
|
+
a1 = gds.FloatItem("a1", default=1.0)
|
|
1283
|
+
a4 = gds.FloatItem("a4", default=0.0).set_pos(col=1)
|
|
1284
|
+
a2 = gds.FloatItem("a2", default=0.0)
|
|
1285
|
+
a5 = gds.FloatItem("a5", default=0.0).set_pos(col=1)
|
|
1286
|
+
|
|
1287
|
+
def generate_title(self) -> str:
|
|
1288
|
+
"""Generate a title based on current parameters."""
|
|
1289
|
+
return (
|
|
1290
|
+
f"polynomial(a0={self.a0:.3g},a1={self.a1:.3g},a2={self.a2:.3g},"
|
|
1291
|
+
f"a3={self.a3:.3g},a4={self.a4:.3g},a5={self.a5:.3g})"
|
|
1292
|
+
)
|
|
1293
|
+
|
|
1294
|
+
def generate_1d_data(self) -> tuple[np.ndarray, np.ndarray]:
|
|
1295
|
+
"""Compute 1D data based on current parameters.
|
|
1296
|
+
|
|
1297
|
+
Returns:
|
|
1298
|
+
Tuple of (x, y) arrays
|
|
1299
|
+
"""
|
|
1300
|
+
x = self.generate_x_data()
|
|
1301
|
+
y = np.polyval([self.a5, self.a4, self.a3, self.a2, self.a1, self.a0], x)
|
|
1302
|
+
return x, y
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
register_signal_parameters_class(SignalTypes.POLYNOMIAL, PolyParam)
|
|
1306
|
+
|
|
1307
|
+
|
|
1308
|
+
class CustomSignalParam(NewSignalParam, title=_("Custom signal")):
|
|
1309
|
+
"""Parameters for custom signal (e.g. manually defined experimental data)."""
|
|
1310
|
+
|
|
1311
|
+
size = gds.IntItem(_("N<sub>points</sub>"), default=10).set_prop(
|
|
1312
|
+
"display", active=False
|
|
1313
|
+
)
|
|
1314
|
+
xmin = gds.FloatItem("x<sub>min</sub>", default=0.0).set_prop(
|
|
1315
|
+
"display", active=False
|
|
1316
|
+
)
|
|
1317
|
+
xmax = gds.FloatItem("x<sub>max</sub>", default=1.0).set_prop(
|
|
1318
|
+
"display", active=False, col=1
|
|
1319
|
+
)
|
|
1320
|
+
|
|
1321
|
+
xyarray = gds.FloatArrayItem(
|
|
1322
|
+
"XY Values",
|
|
1323
|
+
format="%g",
|
|
1324
|
+
)
|
|
1325
|
+
|
|
1326
|
+
def setup_array(
|
|
1327
|
+
self,
|
|
1328
|
+
size: int | None = None,
|
|
1329
|
+
xmin: float | None = None,
|
|
1330
|
+
xmax: float | None = None,
|
|
1331
|
+
) -> None:
|
|
1332
|
+
"""Setup the xyarray from size, xmin and xmax (use the current values is not
|
|
1333
|
+
provided)
|
|
1334
|
+
|
|
1335
|
+
Args:
|
|
1336
|
+
size: xyarray size (default: None)
|
|
1337
|
+
xmin: X min (default: None)
|
|
1338
|
+
xmax: X max (default: None)
|
|
1339
|
+
"""
|
|
1340
|
+
self.size = size or self.size
|
|
1341
|
+
self.xmin = xmin or self.xmin
|
|
1342
|
+
self.xmax = xmax or self.xmax
|
|
1343
|
+
x_arr = np.linspace(self.xmin, self.xmax, self.size) # type: ignore
|
|
1344
|
+
self.xyarray = np.vstack((x_arr, x_arr)).T
|
|
1345
|
+
|
|
1346
|
+
def generate_title(self) -> str:
|
|
1347
|
+
"""Generate a title based on current parameters."""
|
|
1348
|
+
return f"custom(size={self.size})"
|
|
1349
|
+
|
|
1350
|
+
def generate_1d_data(self) -> tuple[np.ndarray, np.ndarray]:
|
|
1351
|
+
"""Compute 1D data based on current parameters.
|
|
1352
|
+
|
|
1353
|
+
Returns:
|
|
1354
|
+
Tuple of (x, y) arrays
|
|
1355
|
+
"""
|
|
1356
|
+
self.setup_array(size=self.size, xmin=self.xmin, xmax=self.xmax)
|
|
1357
|
+
x, y = self.xyarray.T
|
|
1358
|
+
return x, y
|
|
1359
|
+
|
|
1360
|
+
|
|
1361
|
+
register_signal_parameters_class(SignalTypes.CUSTOM, CustomSignalParam)
|
|
1362
|
+
|
|
1363
|
+
|
|
1364
|
+
check_all_signal_parameters_classes()
|
|
1365
|
+
|
|
1366
|
+
|
|
1367
|
+
def triangle_func(xarr: np.ndarray) -> np.ndarray:
|
|
1368
|
+
"""Triangle function
|
|
1369
|
+
|
|
1370
|
+
Args:
|
|
1371
|
+
xarr: x data
|
|
1372
|
+
"""
|
|
1373
|
+
# ignore warning, as type hint is not handled properly in upstream library
|
|
1374
|
+
return sps.sawtooth(xarr, width=0.5) # type: ignore[no-untyped-def]
|
|
1375
|
+
|
|
1376
|
+
|
|
1377
|
+
SIG_NB = 0
|
|
1378
|
+
|
|
1379
|
+
|
|
1380
|
+
def get_next_signal_number() -> int:
|
|
1381
|
+
"""Get the next signal number.
|
|
1382
|
+
|
|
1383
|
+
This function is used to keep track of the number of signals created.
|
|
1384
|
+
It is typically used to generate unique titles for new signals.
|
|
1385
|
+
|
|
1386
|
+
Returns:
|
|
1387
|
+
int: new signal number
|
|
1388
|
+
"""
|
|
1389
|
+
global SIG_NB # pylint: disable=global-statement
|
|
1390
|
+
SIG_NB += 1
|
|
1391
|
+
return SIG_NB
|
|
1392
|
+
|
|
1393
|
+
|
|
1394
|
+
def create_signal_from_param(param: NewSignalParam) -> SignalObj:
|
|
1395
|
+
"""Create a new Signal object from parameters.
|
|
1396
|
+
|
|
1397
|
+
Args:
|
|
1398
|
+
param: new signal parameters
|
|
1399
|
+
|
|
1400
|
+
Returns:
|
|
1401
|
+
Signal object
|
|
1402
|
+
|
|
1403
|
+
Raises:
|
|
1404
|
+
NotImplementedError: if the signal type is not supported
|
|
1405
|
+
"""
|
|
1406
|
+
# Generate data first, as some `generate_title()` methods may depend on it:
|
|
1407
|
+
x, y = param.generate_1d_data()
|
|
1408
|
+
# Check if user has customized the title or left it as default/empty
|
|
1409
|
+
use_generated_title = not param.title or param.title == DEFAULT_TITLE
|
|
1410
|
+
if use_generated_title:
|
|
1411
|
+
# Try to generate a descriptive title
|
|
1412
|
+
gen_title = getattr(param, "generate_title", lambda: "")()
|
|
1413
|
+
if gen_title:
|
|
1414
|
+
title = gen_title
|
|
1415
|
+
else:
|
|
1416
|
+
# No generated title available, use default with number
|
|
1417
|
+
title = f"{DEFAULT_TITLE} {get_next_signal_number():d}"
|
|
1418
|
+
else:
|
|
1419
|
+
# User has set a custom title, use it as-is
|
|
1420
|
+
title = param.title
|
|
1421
|
+
signal = create_signal(
|
|
1422
|
+
title,
|
|
1423
|
+
x,
|
|
1424
|
+
y,
|
|
1425
|
+
units=(param.xunit, param.yunit),
|
|
1426
|
+
labels=(param.xlabel, param.ylabel),
|
|
1427
|
+
)
|
|
1428
|
+
return signal
|