Skip to content

Commit 8339ba8

Browse files
committed
add noise feature
1 parent 4ec28a1 commit 8339ba8

File tree

62 files changed

+740
-80
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+740
-80
lines changed

src/surfaces/noise/__init__.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Author: Simon Blanke
2+
3+
# License: MIT License
4+
5+
"""Noise layers for adding stochastic disturbances to test functions.
6+
7+
This module provides noise classes that can be passed to test functions
8+
to simulate noisy evaluations. Useful for testing algorithm robustness
9+
to measurement uncertainty.
10+
11+
Examples
12+
--------
13+
Basic usage with a test function:
14+
15+
>>> from surfaces.test_functions import SphereFunction
16+
>>> from surfaces.noise import GaussianNoise
17+
>>>
18+
>>> noise = GaussianNoise(sigma=0.1, seed=42)
19+
>>> func = SphereFunction(n_dim=2, noise=noise)
20+
>>> result = func([0.5, 0.5]) # Returns noisy evaluation
21+
22+
Decaying noise over optimization:
23+
24+
>>> noise = GaussianNoise(
25+
... sigma=0.5,
26+
... sigma_final=0.01,
27+
... schedule="linear",
28+
... total_evaluations=1000,
29+
... seed=42
30+
... )
31+
>>> func = SphereFunction(n_dim=2, noise=noise)
32+
33+
Available noise types:
34+
35+
- GaussianNoise: Additive Gaussian noise, f(x) + N(0, sigma^2)
36+
- UniformNoise: Additive uniform noise, f(x) + U(low, high)
37+
- MultiplicativeNoise: Multiplicative noise, f(x) * (1 + N(0, sigma^2))
38+
"""
39+
40+
from ._base import BaseNoise
41+
from ._gaussian import GaussianNoise
42+
from ._multiplicative import MultiplicativeNoise
43+
from ._uniform import UniformNoise
44+
45+
__all__ = [
46+
"BaseNoise",
47+
"GaussianNoise",
48+
"UniformNoise",
49+
"MultiplicativeNoise",
50+
]

src/surfaces/noise/_base.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Author: Simon Blanke
2+
3+
# License: MIT License
4+
5+
"""Base class for noise layers."""
6+
7+
from abc import ABC, abstractmethod
8+
from typing import Any, Dict, Optional
9+
10+
import numpy as np
11+
12+
13+
class BaseNoise(ABC):
14+
"""Base class for noise layers that can be applied to test functions.
15+
16+
Noise layers add stochastic disturbances to function evaluations,
17+
useful for testing algorithm robustness to noisy observations.
18+
19+
Parameters
20+
----------
21+
seed : int, optional
22+
Random seed for reproducibility. If None, uses non-deterministic
23+
random state.
24+
schedule : str, optional
25+
Schedule for decaying noise over evaluations. Options:
26+
- None: Constant noise (default)
27+
- "linear": Linear decay from initial to final
28+
- "exponential": Exponential decay
29+
- "cosine": Cosine annealing
30+
total_evaluations : int, optional
31+
Total number of evaluations for the schedule. Required if
32+
schedule is set.
33+
34+
Attributes
35+
----------
36+
last_noise : float or None
37+
The noise value from the most recent apply() call.
38+
None if apply() has not been called yet.
39+
40+
Examples
41+
--------
42+
>>> from surfaces.noise import GaussianNoise
43+
>>> noise = GaussianNoise(sigma=0.1, seed=42)
44+
>>> noisy_value = noise.apply(5.0, {"x0": 0.5})
45+
>>> print(noise.last_noise) # The noise that was added
46+
"""
47+
48+
def __init__(
49+
self,
50+
seed: Optional[int] = None,
51+
schedule: Optional[str] = None,
52+
total_evaluations: Optional[int] = None,
53+
):
54+
if schedule is not None and total_evaluations is None:
55+
raise ValueError("total_evaluations required when schedule is set")
56+
57+
if schedule is not None and schedule not in ("linear", "exponential", "cosine"):
58+
raise ValueError(
59+
f"schedule must be 'linear', 'exponential', or 'cosine', got '{schedule}'"
60+
)
61+
62+
self._seed = seed
63+
self._rng = np.random.default_rng(seed)
64+
self._schedule = schedule
65+
self._total_evaluations = total_evaluations
66+
self._evaluation_count = 0
67+
self.last_noise: Optional[float] = None
68+
69+
def _get_schedule_factor(self) -> float:
70+
"""Get the current schedule factor in [0, 1].
71+
72+
Returns 1.0 at the start (full noise) and decays toward 0.0
73+
according to the schedule type.
74+
75+
Returns
76+
-------
77+
float
78+
Schedule factor between 0 and 1.
79+
"""
80+
if self._schedule is None:
81+
return 1.0
82+
83+
progress = self._evaluation_count / self._total_evaluations
84+
progress = min(1.0, progress) # Cap at 1.0
85+
86+
if self._schedule == "linear":
87+
return 1.0 - progress
88+
elif self._schedule == "exponential":
89+
# Decays to ~0.0067 at progress=1.0
90+
return np.exp(-5.0 * progress)
91+
elif self._schedule == "cosine":
92+
# Smooth cosine decay
93+
return 0.5 * (1.0 + np.cos(np.pi * progress))
94+
95+
return 1.0
96+
97+
def apply(self, value: float, params: Dict[str, Any]) -> float:
98+
"""Apply noise to a function value.
99+
100+
Parameters
101+
----------
102+
value : float
103+
The original function value.
104+
params : dict
105+
The input parameters (available for heteroscedastic noise).
106+
107+
Returns
108+
-------
109+
float
110+
The noisy function value.
111+
"""
112+
self._evaluation_count += 1
113+
return self._apply_noise(value, params)
114+
115+
@abstractmethod
116+
def _apply_noise(self, value: float, params: Dict[str, Any]) -> float:
117+
"""Apply noise to a value. Override in subclasses.
118+
119+
Parameters
120+
----------
121+
value : float
122+
The original function value.
123+
params : dict
124+
The input parameters.
125+
126+
Returns
127+
-------
128+
float
129+
The noisy function value.
130+
"""
131+
pass
132+
133+
def reset(self, seed: Optional[int] = None) -> None:
134+
"""Reset the noise layer state.
135+
136+
Resets the evaluation counter and random state.
137+
138+
Parameters
139+
----------
140+
seed : int, optional
141+
New seed for the random state. If None, uses the original seed.
142+
"""
143+
self._evaluation_count = 0
144+
self._rng = np.random.default_rng(seed if seed is not None else self._seed)
145+
self.last_noise = None
146+
147+
@property
148+
def evaluation_count(self) -> int:
149+
"""Number of times apply() has been called."""
150+
return self._evaluation_count

src/surfaces/noise/_gaussian.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Author: Simon Blanke
2+
3+
# License: MIT License
4+
5+
"""Gaussian (additive normal) noise."""
6+
7+
from typing import Any, Dict, Optional
8+
9+
from ._base import BaseNoise
10+
11+
12+
class GaussianNoise(BaseNoise):
13+
"""Additive Gaussian noise: f(x) + N(0, sigma^2).
14+
15+
Adds normally distributed noise to function evaluations.
16+
Supports optional scheduling to decay sigma over evaluations.
17+
18+
Parameters
19+
----------
20+
sigma : float, default=0.1
21+
Standard deviation of the Gaussian noise. This is the initial
22+
value if a schedule is used.
23+
sigma_final : float, optional
24+
Final standard deviation when using a schedule. If None,
25+
defaults to sigma (no decay in the sigma value itself,
26+
but schedule factor still applies).
27+
seed : int, optional
28+
Random seed for reproducibility.
29+
schedule : str, optional
30+
Schedule for decaying noise. See BaseNoise for options.
31+
total_evaluations : int, optional
32+
Total evaluations for the schedule.
33+
34+
Attributes
35+
----------
36+
last_noise : float or None
37+
The noise value added in the most recent apply() call.
38+
39+
Examples
40+
--------
41+
Constant noise:
42+
43+
>>> noise = GaussianNoise(sigma=0.1, seed=42)
44+
>>> noisy = noise.apply(5.0, {})
45+
>>> print(f"Added noise: {noise.last_noise:.4f}")
46+
47+
Decaying noise (sigma: 0.5 -> 0.01 over 1000 evaluations):
48+
49+
>>> noise = GaussianNoise(
50+
... sigma=0.5,
51+
... sigma_final=0.01,
52+
... schedule="linear",
53+
... total_evaluations=1000,
54+
... seed=42
55+
... )
56+
"""
57+
58+
def __init__(
59+
self,
60+
sigma: float = 0.1,
61+
sigma_final: Optional[float] = None,
62+
seed: Optional[int] = None,
63+
schedule: Optional[str] = None,
64+
total_evaluations: Optional[int] = None,
65+
):
66+
super().__init__(
67+
seed=seed,
68+
schedule=schedule,
69+
total_evaluations=total_evaluations,
70+
)
71+
72+
if sigma < 0:
73+
raise ValueError(f"sigma must be non-negative, got {sigma}")
74+
75+
self._sigma_initial = sigma
76+
self._sigma_final = sigma_final if sigma_final is not None else sigma
77+
78+
if self._sigma_final < 0:
79+
raise ValueError(f"sigma_final must be non-negative, got {sigma_final}")
80+
81+
@property
82+
def sigma(self) -> float:
83+
"""Current sigma based on schedule progress."""
84+
factor = self._get_schedule_factor()
85+
return self._sigma_final + (self._sigma_initial - self._sigma_final) * factor
86+
87+
def _apply_noise(self, value: float, params: Dict[str, Any]) -> float:
88+
"""Apply Gaussian noise to the value."""
89+
self.last_noise = self._rng.normal(0.0, self.sigma)
90+
return value + self.last_noise
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Author: Simon Blanke
2+
3+
# License: MIT License
4+
5+
"""Multiplicative noise."""
6+
7+
from typing import Any, Dict, Optional
8+
9+
from ._base import BaseNoise
10+
11+
12+
class MultiplicativeNoise(BaseNoise):
13+
"""Multiplicative Gaussian noise: f(x) * (1 + N(0, sigma^2)).
14+
15+
Applies noise proportional to the function value. Useful for
16+
simulating relative measurement uncertainty where larger values
17+
have proportionally larger noise.
18+
19+
Parameters
20+
----------
21+
sigma : float, default=0.1
22+
Standard deviation of the multiplicative factor. A value of 0.1
23+
means the noise factor is typically within +/-10% of 1.0.
24+
This is the initial value if a schedule is used.
25+
sigma_final : float, optional
26+
Final sigma when using a schedule. Defaults to sigma.
27+
seed : int, optional
28+
Random seed for reproducibility.
29+
schedule : str, optional
30+
Schedule for decaying noise. See BaseNoise for options.
31+
total_evaluations : int, optional
32+
Total evaluations for the schedule.
33+
34+
Attributes
35+
----------
36+
last_noise : float or None
37+
The multiplicative factor (not the final noise contribution)
38+
from the most recent apply() call. The actual noise added
39+
is value * last_noise.
40+
41+
Examples
42+
--------
43+
Constant multiplicative noise (+/-10%):
44+
45+
>>> noise = MultiplicativeNoise(sigma=0.1, seed=42)
46+
>>> noisy = noise.apply(100.0, {})
47+
>>> # Result is approximately 100 * (1 + small_gaussian)
48+
49+
Note that for values near zero, multiplicative noise has minimal
50+
effect. Use GaussianNoise for additive noise that is independent
51+
of the function value.
52+
"""
53+
54+
def __init__(
55+
self,
56+
sigma: float = 0.1,
57+
sigma_final: Optional[float] = None,
58+
seed: Optional[int] = None,
59+
schedule: Optional[str] = None,
60+
total_evaluations: Optional[int] = None,
61+
):
62+
super().__init__(
63+
seed=seed,
64+
schedule=schedule,
65+
total_evaluations=total_evaluations,
66+
)
67+
68+
if sigma < 0:
69+
raise ValueError(f"sigma must be non-negative, got {sigma}")
70+
71+
self._sigma_initial = sigma
72+
self._sigma_final = sigma_final if sigma_final is not None else sigma
73+
74+
if self._sigma_final < 0:
75+
raise ValueError(f"sigma_final must be non-negative, got {sigma_final}")
76+
77+
@property
78+
def sigma(self) -> float:
79+
"""Current sigma based on schedule progress."""
80+
factor = self._get_schedule_factor()
81+
return self._sigma_final + (self._sigma_initial - self._sigma_final) * factor
82+
83+
def _apply_noise(self, value: float, params: Dict[str, Any]) -> float:
84+
"""Apply multiplicative noise to the value."""
85+
self.last_noise = self._rng.normal(0.0, self.sigma)
86+
return value * (1.0 + self.last_noise)

0 commit comments

Comments
 (0)