Skip to content

Commit be69c34

Browse files
committed
Refactor noise module. Add enums for noise types.
Refactor noise related tests.
1 parent a62270d commit be69c34

File tree

5 files changed

+215
-79
lines changed

5 files changed

+215
-79
lines changed

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ v2.0.0
88

99
Unreleased
1010
------------------
11+
Added
12+
- Added `tcod.noise.Algorithm` and `tcod.noise.Implementation` enums.
13+
14+
Deprecated
15+
- The non-enum noise implementation names have been deprecated.
16+
1117
Fixed
1218
- Indexing Noise classes now works with the FBM implementation.
1319

docs/tcod/noise.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ tcod.noise - Noise Map Generators
33

44
.. automodule:: tcod.noise
55
:members:
6+
:member-order: bysource

tcod/noise.py

Lines changed: 106 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
1111
noise = tcod.noise.Noise(
1212
dimensions=2,
13-
algorithm=tcod.NOISE_SIMPLEX,
14-
implementation=tcod.noise.TURBULENCE,
13+
algorithm=tcod.noise.Algorithm.SIMPLEX,
14+
implementation=tcod.noise.Implementation.TURBULENCE,
1515
hurst=0.5,
1616
lacunarity=2.0,
1717
octaves=4,
@@ -31,7 +31,9 @@
3131
samples = noise.sample_ogrid(ogrid)
3232
print(samples)
3333
"""
34-
from typing import Any, Optional
34+
import enum
35+
import warnings
36+
from typing import Any, Optional, Sequence, Union
3537

3638
import numpy as np
3739

@@ -40,10 +42,63 @@
4042
from tcod._internal import deprecate
4143
from tcod.loader import ffi, lib
4244

43-
"""Noise implementation constants"""
44-
SIMPLE = 0
45-
FBM = 1
46-
TURBULENCE = 2
45+
try:
46+
from numpy.typing import ArrayLike
47+
except ImportError: # Python < 3.7, Numpy < 1.20
48+
from typing import Any as ArrayLike
49+
50+
51+
class Algorithm(enum.IntEnum):
52+
"""Libtcod noise algorithms.
53+
54+
.. versionadded:: 12.2
55+
"""
56+
57+
PERLIN = 1
58+
"""Perlin noise."""
59+
60+
SIMPLEX = 2
61+
"""Simplex noise."""
62+
63+
WAVELET = 4
64+
"""Wavelet noise."""
65+
66+
def __repr__(self) -> str:
67+
return f"tcod.noise.Algorithm.{self.name}"
68+
69+
70+
class Implementation(enum.IntEnum):
71+
"""Noise implementations.
72+
73+
.. versionadded:: 12.2
74+
"""
75+
76+
SIMPLE = 0
77+
"""Generate plain noise."""
78+
79+
FBM = 1
80+
"""Fractional Brownian motion.
81+
82+
https://en.wikipedia.org/wiki/Fractional_Brownian_motion
83+
"""
84+
85+
TURBULENCE = 2
86+
"""Turbulence noise implementation."""
87+
88+
def __repr__(self) -> str:
89+
return f"tcod.noise.Implementation.{self.name}"
90+
91+
92+
def __getattr__(name: str) -> Implementation:
93+
if hasattr(Implementation, name):
94+
warnings.warn(
95+
f"'tcod.noise.{name}' is deprecated,"
96+
f" use 'tcod.noise.Implementation.{name}' instead.",
97+
DeprecationWarning,
98+
stacklevel=2,
99+
)
100+
return Implementation[name]
101+
raise AttributeError(f"module {__name__} has no attribute {name}")
47102

48103

49104
class Noise(object):
@@ -59,8 +114,9 @@ class Noise(object):
59114
60115
Args:
61116
dimensions (int): Must be from 1 to 4.
62-
algorithm (int): Defaults to NOISE_SIMPLEX
63-
implementation (int): Defaults to tcod.noise.SIMPLE
117+
algorithm (int): Defaults to :any:`tcod.noise.Algorithm.SIMPLEX`
118+
implementation (int):
119+
Defaults to :any:`tcod.noise.Implementation.SIMPLE`
64120
hurst (float): The hurst exponent. Should be in the 0.0-1.0 range.
65121
lacunarity (float): The noise lacunarity.
66122
octaves (float): The level of detail on fBm and turbulence
@@ -74,21 +130,21 @@ class Noise(object):
74130
def __init__(
75131
self,
76132
dimensions: int,
77-
algorithm: int = 2,
78-
implementation: int = SIMPLE,
133+
algorithm: int = Algorithm.SIMPLEX,
134+
implementation: int = Implementation.SIMPLE,
79135
hurst: float = 0.5,
80136
lacunarity: float = 2.0,
81137
octaves: float = 4,
82-
seed: Optional[tcod.random.Random] = None,
138+
seed: Optional[Union[int, tcod.random.Random]] = None,
83139
):
84140
if not 0 < dimensions <= 4:
85141
raise ValueError(
86142
"dimensions must be in range 0 < n <= 4, got %r"
87143
% (dimensions,)
88144
)
89-
self._random = seed
90-
_random_c = seed.random_c if seed else ffi.NULL
91-
self._algorithm = algorithm
145+
self._seed = seed
146+
self._random = self.__rng_from_seed(seed)
147+
_random_c = self._random.random_c
92148
self.noise_c = ffi.gc(
93149
ffi.cast(
94150
"struct TCOD_Noise*",
@@ -99,8 +155,35 @@ def __init__(
99155
self._tdl_noise_c = ffi.new(
100156
"TDLNoise*", (self.noise_c, dimensions, 0, octaves)
101157
)
158+
self.algorithm = algorithm
102159
self.implementation = implementation # sanity check
103160

161+
@staticmethod
162+
def __rng_from_seed(
163+
seed: Union[None, int, tcod.random.Random]
164+
) -> tcod.random.Random:
165+
if seed is None or isinstance(seed, int):
166+
return tcod.random.Random(
167+
seed=seed, algorithm=tcod.random.MERSENNE_TWISTER
168+
)
169+
return seed
170+
171+
def __repr__(self) -> str:
172+
parameters = [
173+
f"dimensions={self.dimensions}",
174+
f"algorithm={self.algorithm!r}",
175+
f"implementation={Implementation(self.implementation)!r}",
176+
]
177+
if self.hurst != 0.5:
178+
parameters.append(f"hurst={self.hurst}")
179+
if self.lacunarity != 2:
180+
parameters.append(f"lacunarity={self.lacunarity}")
181+
if self.octaves != 4:
182+
parameters.append(f"octaves={self.octaves}")
183+
if self._seed is not None:
184+
parameters.append(f"seed={self._seed}")
185+
return f"tcod.noise.Noise({', '.join(parameters)})"
186+
104187
@property
105188
def dimensions(self) -> int:
106189
return int(self._tdl_noise_c.dimensions)
@@ -112,15 +195,16 @@ def dimentions(self) -> int:
112195

113196
@property
114197
def algorithm(self) -> int:
115-
return int(self.noise_c.noise_type)
198+
noise_type = self.noise_c.noise_type
199+
return Algorithm(noise_type) if noise_type else Algorithm.SIMPLEX
116200

117201
@algorithm.setter
118202
def algorithm(self, value: int) -> None:
119203
lib.TCOD_noise_set_type(self.noise_c, value)
120204

121205
@property
122206
def implementation(self) -> int:
123-
return int(self._tdl_noise_c.implementation)
207+
return Implementation(self._tdl_noise_c.implementation)
124208

125209
@implementation.setter
126210
def implementation(self, value: int) -> None:
@@ -181,15 +265,15 @@ def __getitem__(self, indexes: Any) -> np.ndarray:
181265
c_input[i] = ffi.from_buffer("float*", indexes[i])
182266

183267
out = np.empty(indexes[0].shape, dtype=np.float32)
184-
if self.implementation == SIMPLE:
268+
if self.implementation == Implementation.SIMPLE:
185269
lib.TCOD_noise_get_vectorized(
186270
self.noise_c,
187271
self.algorithm,
188272
out.size,
189273
*c_input,
190274
ffi.from_buffer("float*", out),
191275
)
192-
elif self.implementation == FBM:
276+
elif self.implementation == Implementation.FBM:
193277
lib.TCOD_noise_get_fbm_vectorized(
194278
self.noise_c,
195279
self.algorithm,
@@ -198,7 +282,7 @@ def __getitem__(self, indexes: Any) -> np.ndarray:
198282
*c_input,
199283
ffi.from_buffer("float*", out),
200284
)
201-
elif self.implementation == TURBULENCE:
285+
elif self.implementation == Implementation.TURBULENCE:
202286
lib.TCOD_noise_get_turbulence_vectorized(
203287
self.noise_c,
204288
self.algorithm,
@@ -212,7 +296,7 @@ def __getitem__(self, indexes: Any) -> np.ndarray:
212296

213297
return out
214298

215-
def sample_mgrid(self, mgrid: np.ndarray) -> np.ndarray:
299+
def sample_mgrid(self, mgrid: ArrayLike) -> np.ndarray:
216300
"""Sample a mesh-grid array and return the result.
217301
218302
The :any:`sample_ogrid` method performs better as there is a lot of
@@ -248,7 +332,7 @@ def sample_mgrid(self, mgrid: np.ndarray) -> np.ndarray:
248332
)
249333
return out
250334

251-
def sample_ogrid(self, ogrid: np.ndarray) -> np.ndarray:
335+
def sample_ogrid(self, ogrid: Sequence[ArrayLike]) -> np.ndarray:
252336
"""Sample an open mesh-grid array and return the result.
253337
254338
Args

tests/test_noise.py

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,102 @@
1-
#!/usr/bin/env python
1+
import copy
2+
import pickle
3+
4+
import numpy as np
5+
import pytest
6+
7+
import tcod
8+
9+
10+
@pytest.mark.parametrize("implementation", tcod.noise.Implementation)
11+
@pytest.mark.parametrize("algorithm", tcod.noise.Algorithm)
12+
@pytest.mark.parametrize("hurst", [0.5, 0.75])
13+
@pytest.mark.parametrize("lacunarity", [2, 3])
14+
@pytest.mark.parametrize("octaves", [4, 6])
15+
def test_noise_class(
16+
implementation: tcod.noise.Implementation,
17+
algorithm: tcod.noise.Algorithm,
18+
hurst: float,
19+
lacunarity: float,
20+
octaves: float,
21+
) -> None:
22+
noise = tcod.noise.Noise(
23+
2,
24+
algorithm=algorithm,
25+
implementation=implementation,
26+
hurst=hurst,
27+
lacunarity=lacunarity,
28+
octaves=octaves,
29+
)
30+
# cover attributes
31+
assert noise.dimensions == 2
32+
noise.algorithm = noise.algorithm
33+
noise.implementation = noise.implementation
34+
noise.octaves = noise.octaves
35+
assert noise.hurst
36+
assert noise.lacunarity
37+
38+
assert noise.get_point(0, 0) == noise[0, 0]
39+
assert noise[0] == noise[0, 0]
40+
noise.sample_mgrid(np.mgrid[:2, :3])
41+
noise.sample_ogrid(np.ogrid[:2, :3])
42+
43+
np.testing.assert_equal(
44+
noise.sample_mgrid(np.mgrid[:2, :3]),
45+
noise.sample_ogrid(np.ogrid[:2, :3]),
46+
)
47+
np.testing.assert_equal(
48+
noise.sample_mgrid(np.mgrid[:2, :3]), noise[tuple(np.mgrid[:2, :3])]
49+
)
50+
repr(noise)
51+
52+
53+
def test_noise_samples() -> None:
54+
noise = tcod.noise.Noise(
55+
2, tcod.noise.Algorithm.SIMPLEX, tcod.noise.Implementation.SIMPLE
56+
)
57+
np.testing.assert_equal(
58+
noise.sample_mgrid(np.mgrid[:32, :24]),
59+
noise.sample_ogrid(np.ogrid[:32, :24]),
60+
)
61+
62+
63+
def test_noise_errors() -> None:
64+
with pytest.raises(ValueError):
65+
tcod.noise.Noise(0)
66+
with pytest.raises(ValueError):
67+
tcod.noise.Noise(1, implementation=-1)
68+
noise = tcod.noise.Noise(2)
69+
with pytest.raises(ValueError):
70+
noise.sample_mgrid(np.mgrid[:2, :2, :2])
71+
with pytest.raises(ValueError):
72+
noise.sample_ogrid(np.ogrid[:2, :2, :2])
73+
with pytest.raises(IndexError):
74+
noise[0, 0, 0, 0, 0]
75+
with pytest.raises(TypeError):
76+
noise[object]
77+
78+
79+
@pytest.mark.parametrize("implementation", tcod.noise.Implementation)
80+
def test_noise_pickle(implementation: tcod.noise.Implementation) -> None:
81+
rand = tcod.random.Random(tcod.random.MERSENNE_TWISTER, 42)
82+
noise = tcod.noise.Noise(2, implementation, seed=rand)
83+
noise2 = copy.copy(noise)
84+
np.testing.assert_equal(
85+
noise.sample_ogrid(np.ogrid[:3, :1]),
86+
noise2.sample_ogrid(np.ogrid[:3, :1]),
87+
)
88+
89+
90+
def test_noise_copy() -> None:
91+
rand = tcod.random.Random(tcod.random.MERSENNE_TWISTER, 42)
92+
noise = tcod.noise.Noise(2, seed=rand)
93+
noise2 = pickle.loads(pickle.dumps(noise))
94+
np.testing.assert_equal(
95+
noise.sample_ogrid(np.ogrid[:3, :1]),
96+
noise2.sample_ogrid(np.ogrid[:3, :1]),
97+
)
98+
99+
noise3 = tcod.noise.Noise(2, seed=None)
100+
assert repr(noise3) == repr(pickle.loads(pickle.dumps(noise3)))
101+
noise4 = tcod.noise.Noise(2, seed=42)
102+
assert repr(noise4) == repr(pickle.loads(pickle.dumps(noise4)))

0 commit comments

Comments
 (0)