euler-preprocess 3.2.0__tar.gz → 3.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/PKG-INFO +34 -1
  2. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/README.md +33 -0
  3. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/fog/capture.py +335 -0
  4. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/fog/transform.py +670 -48
  5. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess.egg-info/PKG-INFO +34 -1
  6. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/pyproject.toml +1 -1
  7. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/tests/test_fog_aux_outputs.py +208 -0
  8. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/__init__.py +0 -0
  9. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/cli.py +0 -0
  10. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/common/__init__.py +0 -0
  11. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/common/dataset.py +0 -0
  12. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/common/device.py +0 -0
  13. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/common/intrinsics.py +0 -0
  14. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/common/io.py +0 -0
  15. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/common/logging.py +0 -0
  16. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/common/noise.py +0 -0
  17. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/common/normalize.py +0 -0
  18. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/common/output.py +0 -0
  19. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/common/sampling.py +0 -0
  20. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/common/transform.py +0 -0
  21. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/fog/__init__.py +0 -0
  22. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/fog/airlight_from_sky.py +0 -0
  23. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/fog/atmospheric_light.py +0 -0
  24. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/fog/augmentations.py +0 -0
  25. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/fog/dcp_airlight.py +0 -0
  26. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/fog/dcp_airlight_torch.py +0 -0
  27. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/fog/dcp_heuristic_airlight.py +0 -0
  28. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/fog/dcp_heuristic_airlight_torch.py +0 -0
  29. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/fog/foggify.py +0 -0
  30. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/fog/foggify_logging.py +0 -0
  31. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/fog/inference.py +0 -0
  32. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/fog/logging.py +0 -0
  33. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/fog/models.py +0 -0
  34. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/fog/pipeline.py +0 -0
  35. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/radial/__init__.py +0 -0
  36. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/radial/transform.py +0 -0
  37. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/sky_depth/__init__.py +0 -0
  38. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess/sky_depth/transform.py +0 -0
  39. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess.egg-info/SOURCES.txt +0 -0
  40. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess.egg-info/dependency_links.txt +0 -0
  41. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess.egg-info/entry_points.txt +0 -0
  42. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess.egg-info/requires.txt +0 -0
  43. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/euler_preprocess.egg-info/top_level.txt +0 -0
  44. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/setup.cfg +0 -0
  45. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/tests/test_airlight_fallback.py +0 -0
  46. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/tests/test_cli_sample_selection.py +0 -0
  47. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/tests/test_dcp_heuristic_airlight.py +0 -0
  48. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/tests/test_foggify_integration.py +0 -0
  49. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/tests/test_radial.py +0 -0
  50. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/tests/test_sky_depth.py +0 -0
  51. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/tests/test_source_backed_output.py +0 -0
  52. {euler_preprocess-3.2.0 → euler_preprocess-3.4.0}/tests/test_zip_output.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: euler-preprocess
3
- Version: 3.2.0
3
+ Version: 3.4.0
4
4
  Summary: Physics-based preprocessing (fog, etc.) for RGB+depth datasets
5
5
  Requires-Python: >=3.9
6
6
  Description-Content-Type: text/markdown
@@ -148,6 +148,7 @@ Controls the fog simulation.
148
148
  "depth_scale": 1.0,
149
149
  "resize_depth": true,
150
150
  "contrast_threshold": 0.05,
151
+ "mode": "sample",
151
152
  "device": "cpu",
152
153
  "gpu_batch_size": 4,
153
154
  "capture": { "preset": "camera" },
@@ -165,6 +166,7 @@ Controls the fog simulation.
165
166
  | `depth_scale` | Multiplier applied to depth values after loading. |
166
167
  | `resize_depth` | Resize the depth map to match the RGB resolution (bilinear). |
167
168
  | `contrast_threshold` | Threshold *C_t* used in the visibility-to-attenuation conversion (default `0.05`). |
169
+ | `mode` | Optional scenario mode. Omit it or use `"sample"` for current one-scenario-per-image behavior; use `"progressive"` to render every scenario step for every image. |
168
170
  | `device` | `"cpu"`, `"cuda"`, `"mps"`, or `"gpu"` (alias for cuda). |
169
171
  | `gpu_batch_size` | Batch size when running on GPU. Uniform-model samples are batched; heterogeneous samples are processed individually. |
170
172
  | `capture` / `capture_artifacts` | Optional post-fog camera artifact pipeline. Omit it or set `{"stages": []}` for the legacy no-op path. Set `true`, `{"preset": "camera"}`, or a custom `stages` list to enable optics, raw sensor, ISP, and compression artifacts. |
@@ -323,6 +325,28 @@ noise is fine-grained rather than blocky. `black_noise_floor` with
323
325
  noise in near-clipped black regions, so the strongest visible noise sits in
324
326
  dim-but-readable shadows.
325
327
 
328
+ Set `sensor.noise_adjustment` for relative, scenario-level noise controls on
329
+ top of the selected camera/condition profile. `level: 1.0` leaves the authored
330
+ profile unchanged; lower values suppress read/static/chroma noise and higher
331
+ values amplify it. `static_chroma_bias` ranges from `-1.0` for more fixed
332
+ pattern, row/column, banding, and bad-pixel noise to `1.0` for more
333
+ chromatic/high-ISO-looking shadow noise:
334
+
335
+ ```json
336
+ {
337
+ "capture_overrides": {
338
+ "sensor": {
339
+ "condition_profile": "nominal_gloom",
340
+ "noise_adjustment": {
341
+ "enabled": true,
342
+ "level": 1.25,
343
+ "static_chroma_bias": 0.35
344
+ }
345
+ }
346
+ }
347
+ }
348
+ ```
349
+
326
350
  Any stage can define `condition_profiles` to sample coherent per-image settings
327
351
  before the stage runs. This is useful for exposure states where ISO, exposure
328
352
  gain, read noise, banding, and dark/fog noise modulation should move together:
@@ -382,6 +406,15 @@ compression together:
382
406
  `condition_profiles`; if omitted, the stage continues sampling its own profile
383
407
  weights locally.
384
408
 
409
+ Set top-level `"mode": "progressive"` to emit every configured scenario for
410
+ every input image instead of sampling one scenario. Each scenario accepts
411
+ `"steps"` and `"weight"`; the transform writes steps from weight `0` through
412
+ the scenario's configured weight, and weight `1` matches the original scenario.
413
+ Fog density is progressed in scattering-coefficient space, while numeric
414
+ camera/config values blend from the base config toward the scenario config.
415
+ Source-backed outputs are written as `fog_progression` variants under each
416
+ source file id.
417
+
385
418
  ### Fog Model
386
419
 
387
420
  The core equation is the **Koschmieder model** (atmospheric scattering):
@@ -134,6 +134,7 @@ Controls the fog simulation.
134
134
  "depth_scale": 1.0,
135
135
  "resize_depth": true,
136
136
  "contrast_threshold": 0.05,
137
+ "mode": "sample",
137
138
  "device": "cpu",
138
139
  "gpu_batch_size": 4,
139
140
  "capture": { "preset": "camera" },
@@ -151,6 +152,7 @@ Controls the fog simulation.
151
152
  | `depth_scale` | Multiplier applied to depth values after loading. |
152
153
  | `resize_depth` | Resize the depth map to match the RGB resolution (bilinear). |
153
154
  | `contrast_threshold` | Threshold *C_t* used in the visibility-to-attenuation conversion (default `0.05`). |
155
+ | `mode` | Optional scenario mode. Omit it or use `"sample"` for current one-scenario-per-image behavior; use `"progressive"` to render every scenario step for every image. |
154
156
  | `device` | `"cpu"`, `"cuda"`, `"mps"`, or `"gpu"` (alias for cuda). |
155
157
  | `gpu_batch_size` | Batch size when running on GPU. Uniform-model samples are batched; heterogeneous samples are processed individually. |
156
158
  | `capture` / `capture_artifacts` | Optional post-fog camera artifact pipeline. Omit it or set `{"stages": []}` for the legacy no-op path. Set `true`, `{"preset": "camera"}`, or a custom `stages` list to enable optics, raw sensor, ISP, and compression artifacts. |
@@ -309,6 +311,28 @@ noise is fine-grained rather than blocky. `black_noise_floor` with
309
311
  noise in near-clipped black regions, so the strongest visible noise sits in
310
312
  dim-but-readable shadows.
311
313
 
314
+ Set `sensor.noise_adjustment` for relative, scenario-level noise controls on
315
+ top of the selected camera/condition profile. `level: 1.0` leaves the authored
316
+ profile unchanged; lower values suppress read/static/chroma noise and higher
317
+ values amplify it. `static_chroma_bias` ranges from `-1.0` for more fixed
318
+ pattern, row/column, banding, and bad-pixel noise to `1.0` for more
319
+ chromatic/high-ISO-looking shadow noise:
320
+
321
+ ```json
322
+ {
323
+ "capture_overrides": {
324
+ "sensor": {
325
+ "condition_profile": "nominal_gloom",
326
+ "noise_adjustment": {
327
+ "enabled": true,
328
+ "level": 1.25,
329
+ "static_chroma_bias": 0.35
330
+ }
331
+ }
332
+ }
333
+ }
334
+ ```
335
+
312
336
  Any stage can define `condition_profiles` to sample coherent per-image settings
313
337
  before the stage runs. This is useful for exposure states where ISO, exposure
314
338
  gain, read noise, banding, and dark/fog noise modulation should move together:
@@ -368,6 +392,15 @@ compression together:
368
392
  `condition_profiles`; if omitted, the stage continues sampling its own profile
369
393
  weights locally.
370
394
 
395
+ Set top-level `"mode": "progressive"` to emit every configured scenario for
396
+ every input image instead of sampling one scenario. Each scenario accepts
397
+ `"steps"` and `"weight"`; the transform writes steps from weight `0` through
398
+ the scenario's configured weight, and weight `1` matches the original scenario.
399
+ Fog density is progressed in scattering-coefficient space, while numeric
400
+ camera/config values blend from the base config toward the scenario config.
401
+ Source-backed outputs are written as `fog_progression` variants under each
402
+ source file id.
403
+
371
404
  ### Fog Model
372
405
 
373
406
  The core equation is the **Koschmieder model** (atmospheric scattering):
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from copy import deepcopy
3
4
  import io
4
5
  import math
5
6
  from dataclasses import dataclass, field
@@ -522,6 +523,7 @@ class SensorStage(ConfiguredCaptureStage):
522
523
  rng,
523
524
  manual_exposure_gain=exposure,
524
525
  )
526
+ config = _apply_sensor_noise_adjustment(config, rng)
525
527
  wb = _sample_triplet(config.get("white_balance", [1.0, 1.0, 1.0]), rng)
526
528
  wb_jitter = _sample_float(config, "white_balance_jitter", 0.0, rng)
527
529
  if wb_jitter > 0.0:
@@ -717,6 +719,7 @@ class SensorStage(ConfiguredCaptureStage):
717
719
  rng,
718
720
  manual_exposure_gain=exposure,
719
721
  )
722
+ config = _apply_sensor_noise_adjustment(config, rng)
720
723
  wb = _sample_triplet(config.get("white_balance", [1.0, 1.0, 1.0]), rng)
721
724
  wb_jitter = _sample_float(config, "white_balance_jitter", 0.0, rng)
722
725
  if wb_jitter > 0.0:
@@ -2152,6 +2155,338 @@ def _resolve_electron_capacity_torch(
2152
2155
  )
2153
2156
 
2154
2157
 
2158
+ def _apply_sensor_noise_adjustment(
2159
+ config: Mapping[str, Any],
2160
+ rng: np.random.Generator,
2161
+ ) -> Mapping[str, Any]:
2162
+ cfg = _config_block(config, "noise_adjustment")
2163
+ if not cfg or not _bool_value(cfg.get("enabled", True)):
2164
+ return config
2165
+
2166
+ level = max(
2167
+ _sample_float_from_keys(
2168
+ cfg,
2169
+ ("level", "amount", "factor", "noise_level"),
2170
+ 1.0,
2171
+ rng,
2172
+ ),
2173
+ 0.0,
2174
+ )
2175
+ bias = float(
2176
+ np.clip(
2177
+ _sample_float_from_keys(
2178
+ cfg,
2179
+ ("static_chroma_bias", "character_bias", "chroma_bias"),
2180
+ 0.0,
2181
+ rng,
2182
+ ),
2183
+ -1.0,
2184
+ 1.0,
2185
+ )
2186
+ )
2187
+ groups = _config_block(cfg, "groups")
2188
+ limits = _config_block(cfg, "limits")
2189
+
2190
+ min_factor = max(_sample_float(limits, "min_factor", 0.0, rng), 0.0)
2191
+ max_factor = max(_sample_float(limits, "max_factor", 8.0, rng), min_factor)
2192
+
2193
+ def group_factor(name: str, base: float) -> float:
2194
+ factor = base * max(_sample_float(groups, name, 1.0, rng), 0.0)
2195
+ return float(np.clip(factor, min_factor, max_factor))
2196
+
2197
+ read_factor = group_factor("read", level * math.pow(2.0, 0.35 * bias))
2198
+ static_factor = group_factor("static", level * math.pow(2.0, -bias))
2199
+ banding_factor = group_factor("banding", static_factor)
2200
+ bad_pixel_factor = group_factor("bad_pixels", static_factor)
2201
+ chroma_factor = group_factor("chroma", level * math.pow(2.0, bias))
2202
+ shadow_luma_factor = group_factor("shadow_luma", level)
2203
+ modulation_factor = group_factor("modulation", 1.0 + (level - 1.0) * 0.75)
2204
+
2205
+ if (
2206
+ abs(read_factor - 1.0) < 1e-9
2207
+ and abs(static_factor - 1.0) < 1e-9
2208
+ and abs(banding_factor - 1.0) < 1e-9
2209
+ and abs(bad_pixel_factor - 1.0) < 1e-9
2210
+ and abs(chroma_factor - 1.0) < 1e-9
2211
+ and abs(shadow_luma_factor - 1.0) < 1e-9
2212
+ and abs(modulation_factor - 1.0) < 1e-9
2213
+ ):
2214
+ return config
2215
+
2216
+ adjusted = deepcopy(dict(config))
2217
+
2218
+ _scale_config_path(adjusted, ("read_noise_electrons",), read_factor, min_value=0.0)
2219
+ _scale_config_path(adjusted, ("read_noise_sigma",), read_factor, min_value=0.0)
2220
+
2221
+ _scale_config_path(adjusted, ("fixed_pattern_sigma",), static_factor, min_value=0.0)
2222
+ _scale_config_path(adjusted, ("row_noise_sigma",), banding_factor, min_value=0.0)
2223
+ _scale_config_path(adjusted, ("column_noise_sigma",), banding_factor, min_value=0.0)
2224
+ _scale_config_path(
2225
+ adjusted,
2226
+ ("banding_modulation",),
2227
+ banding_factor,
2228
+ min_value=0.0,
2229
+ max_value=1.0,
2230
+ )
2231
+
2232
+ max_bad_pixel_probability = _sample_float(
2233
+ limits,
2234
+ "max_bad_pixel_probability",
2235
+ 0.05,
2236
+ rng,
2237
+ )
2238
+ _scale_config_path(
2239
+ adjusted,
2240
+ ("hot_pixel_probability",),
2241
+ bad_pixel_factor,
2242
+ min_value=0.0,
2243
+ max_value=max_bad_pixel_probability,
2244
+ )
2245
+ _scale_config_path(
2246
+ adjusted,
2247
+ ("dead_pixel_probability",),
2248
+ bad_pixel_factor,
2249
+ min_value=0.0,
2250
+ max_value=max_bad_pixel_probability,
2251
+ )
2252
+
2253
+ for key in ("dark_gain", "depth_gain", "fog_gain"):
2254
+ _scale_config_path(
2255
+ adjusted,
2256
+ ("noise_modulation", key),
2257
+ modulation_factor,
2258
+ min_value=0.0,
2259
+ )
2260
+ _scale_config_path(
2261
+ adjusted,
2262
+ ("noise_modulation", "max_gain"),
2263
+ modulation_factor,
2264
+ anchor=1.0,
2265
+ min_value=1.0,
2266
+ )
2267
+
2268
+ _scale_config_path(
2269
+ adjusted,
2270
+ ("shadow_recovery_noise", "luma_sigma"),
2271
+ shadow_luma_factor,
2272
+ min_value=0.0,
2273
+ )
2274
+ _scale_config_path(
2275
+ adjusted,
2276
+ ("shadow_recovery_noise", "chroma_sigma"),
2277
+ chroma_factor,
2278
+ min_value=0.0,
2279
+ )
2280
+ _scale_config_path(
2281
+ adjusted,
2282
+ ("shadow_recovery_noise", "blotch_sigma"),
2283
+ chroma_factor,
2284
+ min_value=0.0,
2285
+ )
2286
+ _scale_config_path(
2287
+ adjusted,
2288
+ ("shadow_recovery_noise", "red_chroma_gain"),
2289
+ chroma_factor,
2290
+ anchor=1.0,
2291
+ min_value=0.0,
2292
+ )
2293
+ _scale_config_path(
2294
+ adjusted,
2295
+ ("shadow_recovery_noise", "blue_chroma_gain"),
2296
+ chroma_factor,
2297
+ anchor=1.0,
2298
+ min_value=0.0,
2299
+ )
2300
+ _scale_config_path(
2301
+ adjusted,
2302
+ ("shadow_recovery_noise", "chroma_axis_correlation"),
2303
+ chroma_factor,
2304
+ min_value=-0.95,
2305
+ max_value=0.95,
2306
+ )
2307
+ return adjusted
2308
+
2309
+
2310
+ def _sample_float_from_keys(
2311
+ config: Mapping[str, Any],
2312
+ keys: tuple[str, ...],
2313
+ default: float,
2314
+ rng: np.random.Generator,
2315
+ ) -> float:
2316
+ for key in keys:
2317
+ if config.get(key) is not None:
2318
+ return _sample_float(config, key, default, rng)
2319
+ return float(default)
2320
+
2321
+
2322
+ def _scale_config_path(
2323
+ config: dict[str, Any],
2324
+ path: tuple[str, ...],
2325
+ factor: float,
2326
+ *,
2327
+ anchor: float = 0.0,
2328
+ min_value: float | None = None,
2329
+ max_value: float | None = None,
2330
+ ) -> None:
2331
+ parent: dict[str, Any] = config
2332
+ for key in path[:-1]:
2333
+ value = parent.get(key)
2334
+ if not isinstance(value, dict):
2335
+ return
2336
+ parent = value
2337
+ key = path[-1]
2338
+ if key not in parent or parent[key] is None:
2339
+ return
2340
+ parent[key] = _scale_numeric_spec(
2341
+ parent[key],
2342
+ factor,
2343
+ anchor=anchor,
2344
+ min_value=min_value,
2345
+ max_value=max_value,
2346
+ )
2347
+
2348
+
2349
+ def _scale_numeric_spec(
2350
+ spec: Any,
2351
+ factor: float,
2352
+ *,
2353
+ anchor: float = 0.0,
2354
+ min_value: float | None = None,
2355
+ max_value: float | None = None,
2356
+ ) -> Any:
2357
+ if isinstance(spec, bool) or spec is None:
2358
+ return spec
2359
+ if isinstance(spec, (int, float)):
2360
+ return _clamp_scaled_number(
2361
+ anchor + (float(spec) - anchor) * factor,
2362
+ min_value,
2363
+ max_value,
2364
+ )
2365
+ if isinstance(spec, list):
2366
+ return [
2367
+ _scale_numeric_spec(
2368
+ value,
2369
+ factor,
2370
+ anchor=anchor,
2371
+ min_value=min_value,
2372
+ max_value=max_value,
2373
+ )
2374
+ for value in spec
2375
+ ]
2376
+ if isinstance(spec, tuple):
2377
+ return tuple(
2378
+ _scale_numeric_spec(
2379
+ value,
2380
+ factor,
2381
+ anchor=anchor,
2382
+ min_value=min_value,
2383
+ max_value=max_value,
2384
+ )
2385
+ for value in spec
2386
+ )
2387
+ if not isinstance(spec, dict):
2388
+ return spec
2389
+
2390
+ scaled = dict(spec)
2391
+ dist = scaled.get("dist")
2392
+ if dist is None:
2393
+ if "value" in scaled:
2394
+ scaled["value"] = _scale_numeric_spec(
2395
+ scaled["value"],
2396
+ factor,
2397
+ anchor=anchor,
2398
+ min_value=min_value,
2399
+ max_value=max_value,
2400
+ )
2401
+ return scaled
2402
+
2403
+ if dist == "constant":
2404
+ scaled["value"] = _scale_numeric_spec(
2405
+ scaled.get("value", 0.0),
2406
+ factor,
2407
+ anchor=anchor,
2408
+ min_value=min_value,
2409
+ max_value=max_value,
2410
+ )
2411
+ elif dist == "uniform":
2412
+ scaled["min"] = _scale_numeric_spec(
2413
+ scaled["min"],
2414
+ factor,
2415
+ anchor=anchor,
2416
+ min_value=min_value,
2417
+ max_value=max_value,
2418
+ )
2419
+ scaled["max"] = _scale_numeric_spec(
2420
+ scaled["max"],
2421
+ factor,
2422
+ anchor=anchor,
2423
+ min_value=min_value,
2424
+ max_value=max_value,
2425
+ )
2426
+ if scaled["min"] > scaled["max"]:
2427
+ scaled["min"], scaled["max"] = scaled["max"], scaled["min"]
2428
+ elif dist == "normal":
2429
+ scaled["mean"] = _scale_numeric_spec(
2430
+ scaled["mean"],
2431
+ factor,
2432
+ anchor=anchor,
2433
+ min_value=min_value,
2434
+ max_value=max_value,
2435
+ )
2436
+ scaled["std"] = max(float(scaled.get("std", 0.0)) * abs(factor), 0.0)
2437
+ for key in ("min", "max"):
2438
+ if key in scaled and scaled[key] is not None:
2439
+ scaled[key] = _scale_numeric_spec(
2440
+ scaled[key],
2441
+ factor,
2442
+ anchor=anchor,
2443
+ min_value=min_value,
2444
+ max_value=max_value,
2445
+ )
2446
+ if scaled.get("min") is not None and scaled.get("max") is not None:
2447
+ if scaled["min"] > scaled["max"]:
2448
+ scaled["min"], scaled["max"] = scaled["max"], scaled["min"]
2449
+ elif dist == "lognormal":
2450
+ if anchor == 0.0 and factor > 0.0:
2451
+ scaled["mean"] = float(scaled.get("mean", 0.0)) + math.log(factor)
2452
+ for key in ("min", "max"):
2453
+ if key in scaled and scaled[key] is not None:
2454
+ scaled[key] = _scale_numeric_spec(
2455
+ scaled[key],
2456
+ factor,
2457
+ anchor=anchor,
2458
+ min_value=min_value,
2459
+ max_value=max_value,
2460
+ )
2461
+ if scaled.get("min") is not None and scaled.get("max") is not None:
2462
+ if scaled["min"] > scaled["max"]:
2463
+ scaled["min"], scaled["max"] = scaled["max"], scaled["min"]
2464
+ elif dist == "choice":
2465
+ scaled["values"] = [
2466
+ _scale_numeric_spec(
2467
+ value,
2468
+ factor,
2469
+ anchor=anchor,
2470
+ min_value=min_value,
2471
+ max_value=max_value,
2472
+ )
2473
+ for value in scaled.get("values", [])
2474
+ ]
2475
+ return scaled
2476
+
2477
+
2478
+ def _clamp_scaled_number(
2479
+ value: float,
2480
+ min_value: float | None,
2481
+ max_value: float | None,
2482
+ ) -> float:
2483
+ if min_value is not None:
2484
+ value = max(float(min_value), value)
2485
+ if max_value is not None:
2486
+ value = min(float(max_value), value)
2487
+ return float(value)
2488
+
2489
+
2155
2490
  def _sensor_noise_modulation(
2156
2491
  signal: np.ndarray,
2157
2492
  context: CaptureContext,