• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Project-OSmOSE / OSEkit / 20991469444

14 Jan 2026 10:51AM UTC coverage: 97.199% (+0.08%) from 97.122%
20991469444

Pull #313

github

web-flow
Merge 6f892bdde into 16da24c64
Pull Request #313: add job submition dependency option

69 of 69 new or added lines in 2 files covered. (100.0%)

16 existing lines in 2 files now uncovered.

4199 of 4320 relevant lines covered (97.2%)

0.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.71
/src/osekit/core_api/frequency_scale.py
1
"""Custom frequency scales for plotting spectrograms.
2

3
The custom scale is formed from a list of ScaleParts, which assign a
4
frequency range to a range on the scale.
5
Provided ScaleParts should cover the whole scale (from 0% to 100%).
6

7
Such Scale can then be passed to the SpectroData.plot() method for the
8
spectrogram to be plotted on a custom frequency scale.
9

10
"""
11

12
from __future__ import annotations
1✔
13

14
from dataclasses import dataclass
1✔
15
from typing import Literal
1✔
16

17
import numpy as np
1✔
18

19
from osekit.utils.core_utils import get_closest_value_index
1✔
20

21

22
@dataclass(frozen=True)
1✔
23
class ScalePart:
1✔
24
    """Represent a part of the frequency scale of a spectrogram.
25

26
    p_min: float
27
        Relative position of the bottom of the scale part on the full scale.
28
        Must be in the interval [0.0, 1.0], where 0.0 is the bottom of the scale
29
        and 1.0 is the top.
30
    p_max: float
31
        Relative position of the top of the scale part on the full scale.
32
        Must be in the interval [0.0, 1.0], where 0.0 is the bottom of the scale
33
        and 1.0 is the top.
34
    f_min: float
35
        Frequency corresponding to the bottom of the scale part.
36
    f_max: float
37
        Frequency corresponding to the top of the scale part.
38
    scale_type: Literal["lin", "log"]
39
        Type of the scale, either linear or logarithmic.
40

41
    """
42

43
    p_min: float
1✔
44
    p_max: float
1✔
45
    f_min: float
1✔
46
    f_max: float
1✔
47
    scale_type: Literal["lin", "log"] = "lin"
1✔
48

49
    def __post_init__(self) -> None:
1✔
50
        """Check if ScalePart values are correct."""
51
        err = []
1✔
52
        if not 0.0 <= self.p_min <= 1.0:
1✔
53
            err.append(f"p_min must be between 0 and 1, got {self.p_min}")
1✔
54
        if not 0.0 <= self.p_max <= 1.0:
1✔
55
            err.append(f"p_max must be between 0 and 1, got {self.p_max}")
1✔
56
        if self.p_min >= self.p_max:
1✔
57
            err.append(
1✔
58
                f"p_min must be strictly inferior than p_max, got ({self.p_min},{self.p_max})",
59
            )
60
        if self.f_min < 0:
1✔
61
            err.append(
1✔
62
                f"f_min must be positive, got {self.f_min}",
63
            )
64
        if self.f_max < 0:
1✔
65
            err.append(
1✔
66
                f"f_max must be positive, got {self.f_max}",
67
            )
68
        if self.f_min >= self.f_max:
1✔
69
            err.append(
1✔
70
                f"f_min must be strictly inferior than f_max, got ({self.f_min},{self.f_max})",
71
            )
72
        if err:
1✔
73
            msg = "\n".join(err)
1✔
74
            raise ValueError(msg)
1✔
75

76
    def get_frequencies(self, nb_points: int) -> list[int]:
1✔
77
        """Return the frequency points of the present scale part."""
78
        space = self.scale_lambda(self.f_min, self.f_max, nb_points)
1✔
79
        return list(map(round, space))
1✔
80

81
    def get_indexes(self, scale_length: int) -> tuple[int, int]:
1✔
82
        """Return the indexes of the present scale part in the full scale."""
83
        return int(self.p_min * scale_length), int(self.p_max * scale_length)
1✔
84

85
    def get_values(self, scale_length: int) -> list[int]:
1✔
86
        """Return the values of the present scale part."""
87
        start, stop = self.get_indexes(scale_length)
1✔
88
        return list(self.scale_lambda(self.f_min, self.f_max, stop - start))
1✔
89

90
    def to_dict_value(self) -> tuple[float, float, float, float, str]:
1✔
91
        """Serialize a ScalePart to a dictionary entry."""
92
        return self.p_min, self.p_max, self.f_min, self.f_max, self.scale_type
1✔
93

94
    def __eq__(self, other: any) -> bool:
1✔
95
        """Overwrite eq dunder."""
96
        if type(other) is not ScalePart:
1✔
UNCOV
97
            return False
×
98
        return (
1✔
99
            self.p_min == other.p_min
100
            and self.p_max == other.p_max
101
            and self.f_min == other.f_min
102
            and self.f_max == other.f_max
103
            and self.scale_type == other.scale_type
104
        )
105

106
    @property
1✔
107
    def scale_lambda(self) -> callable:
1✔
108
        """Lambda function used to generate either a linear or logarithmic scale."""
109
        return lambda start, stop, steps: (
1✔
110
            np.linspace(start, stop, steps)
111
            if self.scale_type == "lin"
112
            else np.geomspace(start, stop, steps)
113
        )
114

115

116
class Scale:
1✔
117
    """Class that represent a custom frequency scale for plotting spectrograms.
118

119
    The custom scale is formed from a list of ScaleParts, which assign a
120
    frequency range to a range on the scale.
121
    Provided ScaleParts should cover the whole scale (from 0% to 100%).
122

123
    Such Scale can then be passed to the SpectroData.plot() method for the
124
    spectrogram to be plotted on a custom frequency scale.
125

126
    """
127

128
    def __init__(self, parts: list[ScalePart]) -> None:
1✔
129
        """Initialize a Scale object."""
130
        self.parts = sorted(parts, key=lambda p: (p.p_min, p.p_max))
1✔
131

132
    def map(self, original_scale_length: int) -> list[float]:
1✔
133
        """Map a given scale to the custom scale defined by its ScaleParts.
134

135
        Parameters
136
        ----------
137
        original_scale_length: int
138
            Length of the original frequency scale.
139

140
        Returns
141
        -------
142
        list[float]
143
            Mapped frequency scale.
144
            Each ScalePart from the Scale.parts attribute are concatenated
145
            to form the returned scale.
146

147
        """
148
        return [
1✔
149
            v for scale in self.parts for v in scale.get_values(original_scale_length)
150
        ]
151

152
    def get_mapped_indexes(self, original_scale: list[float]) -> list[int]:
1✔
153
        """Return the indexes of the present scale in the original scale.
154

155
        The indexes are those of the closest value from the mapped values
156
        in the original scale.
157

158
        Parameters
159
        ----------
160
        original_scale: list[float]
161
            Original scale from which the mapped scale is computed.
162

163
        Returns
164
        -------
165
        list[int]
166
            Indexes of the closest value from the mapped values in the
167
            original scale.
168

169
        """
170
        mapped_scale = self.map(len(original_scale))
1✔
171
        return [
1✔
172
            get_closest_value_index(target=mapped, values=original_scale)
173
            for mapped in mapped_scale
174
        ]
175

176
    def get_mapped_values(self, original_scale: list[float]) -> list[float]:
1✔
177
        """Return the closest values of the mapped scale from the original scale.
178

179
        Parameters
180
        ----------
181
        original_scale: list[float]
182
            Original scale from which the mapped scale is computed.
183

184
        Returns
185
        -------
186
        list[float]
187
            Values from the original scale that are the closest to the mapped scale.
188

189
        """
190
        return [original_scale[i] for i in self.get_mapped_indexes(original_scale)]
1✔
191

192
    def rescale(
1✔
193
        self,
194
        sx_matrix: np.ndarray,
195
        original_scale: np.ndarray | list,
196
    ) -> np.ndarray:
197
        """Rescale the given spectrum matrix according to the present scale.
198

199
        Parameters
200
        ----------
201
        sx_matrix: np.ndarray
202
            Spectrum matrix.
203
        original_scale: np.ndarray
204
            Original frequency axis of the spectrum matrix.
205

206
        Returns
207
        -------
208
        np.ndarray
209
            Spectrum matrix mapped on the present scale.
210

211
        """
212
        if type(original_scale) is np.ndarray:
1✔
UNCOV
213
            original_scale = original_scale.tolist()
×
214

215
        new_scale_indexes = self.get_mapped_indexes(original_scale=original_scale)
1✔
216

217
        return sx_matrix[new_scale_indexes]
1✔
218

219
    def to_dict_value(self) -> list[tuple[float, float, float, float, str]]:
1✔
220
        """Serialize a Scale to a dictionary entry."""
221
        return [part.to_dict_value() for part in self.parts]
1✔
222

223
    @classmethod
1✔
224
    def from_dict_value(cls, dict_value: list[list]) -> Scale:
1✔
225
        """Deserialize a Scale from a dictionary entry."""
226
        return cls([ScalePart(*scale) for scale in dict_value])
1✔
227

228
    def __eq__(self, other: any) -> bool:
1✔
229
        """Overwrite eq dunder."""
230
        if type(other) is not Scale:
1✔
UNCOV
231
            return False
×
232
        return self.parts == other.parts
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc