Skip to content

Commit 404f799

Browse files
Merge branch 'feature-save-load' of github.com:IFCA-Advanced-Computing/frouros
2 parents cfb5e4a + 98709df commit 404f799

File tree

8 files changed

+218
-0
lines changed

8 files changed

+218
-0
lines changed

docs/source/api_reference/utils.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ The {mod}`frouros.utils` module contains auxiliary classes, functions or excepti
88
utils/checks
99
utils/data_structures
1010
utils/kernels
11+
utils/persistence
1112
utils/stats
1213
```
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Persistence
2+
3+
The {mod}`frouros.utils.persistence` module contains auxiliary functions to persistence objects.
4+
5+
```{eval-rst}
6+
.. automodule:: frouros.utils.persistence
7+
:members:
8+
:no-inherited-members:
9+
```

docs/source/examples.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55
66
examples/concept_drift
77
examples/data_drift
8+
examples/utils
89
```

docs/source/examples/utils.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Utils
2+
3+
```{toctree}
4+
:maxdepth: 1
5+
6+
utils/save_load
7+
```

docs/source/faq.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,10 @@ including guidelines for reporting issues, submitting feature requests, and cont
8383

8484
Frouros does not currently provide built-in visualization tools for drift detection results, but it is planned to
8585
include them in future releases.
86+
87+
## How can I persist a detector or callback for later use?
88+
89+
Frouros provides a [persistence module](./api_reference/utils/persistence.html) that allows you to persist a detector
90+
or callback for later use. It offers [save](api_reference/utils/persistence.html#frouros.utils.persistence.save) and
91+
[load](api_reference/utils/persistence.html#frouros.utils.persistence.load) methods to save and load the detector or
92+
callback to and from disk, respectively.
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
"""Test persistence module."""
2+
3+
import pickle
4+
5+
import pytest # type: ignore
6+
7+
from frouros.callbacks import HistoryConceptDrift, PermutationTestDistanceBased
8+
from frouros.callbacks.base import BaseCallback
9+
from frouros.detectors.base import BaseDetector
10+
from frouros.detectors.concept_drift import DDM, DDMConfig
11+
from frouros.detectors.data_drift import MMD
12+
from frouros.utils import load, save
13+
14+
15+
@pytest.fixture(
16+
scope="module",
17+
params=[
18+
DDM(
19+
config=DDMConfig(),
20+
),
21+
MMD(),
22+
],
23+
)
24+
def detector(
25+
request: pytest.FixtureRequest,
26+
) -> BaseDetector:
27+
"""Fixture for detector.
28+
29+
:param request: Request
30+
:type request: pytest.FixtureRequest
31+
:return: Detector
32+
:rtype: BaseDetector
33+
"""
34+
return request.param
35+
36+
37+
@pytest.fixture(
38+
scope="module",
39+
params=[
40+
HistoryConceptDrift(),
41+
PermutationTestDistanceBased(
42+
num_permutations=2,
43+
),
44+
],
45+
)
46+
def callback(
47+
request: pytest.FixtureRequest,
48+
) -> BaseCallback:
49+
"""Fixture for callback.
50+
51+
:param request: Request
52+
:type request: pytest.FixtureRequest
53+
:return: Callback
54+
:rtype: BaseCallback
55+
"""
56+
return request.param
57+
58+
59+
def test_save_load_with_valid_detector(
60+
detector: BaseDetector,
61+
) -> None:
62+
"""Test save and load with valid detector.
63+
64+
:param detector: Detector
65+
:type detector: BaseDetector
66+
"""
67+
filename = "/tmp/detector.pkl"
68+
save(detector, filename)
69+
loaded_detector = load(filename)
70+
assert isinstance(loaded_detector, detector.__class__)
71+
72+
73+
def test_save_load_with_valid_callback(
74+
callback: BaseCallback,
75+
) -> None:
76+
"""Test save and load with valid callback.
77+
78+
:param callback: Callback
79+
:type callback: BaseCallback
80+
"""
81+
filename = "/tmp/callback.pkl"
82+
save(callback, filename)
83+
loaded_callback = load(filename)
84+
assert isinstance(loaded_callback, BaseCallback)
85+
86+
87+
def test_save_with_invalid_object() -> None:
88+
"""Test save with invalid object.
89+
90+
:raises TypeError: Type error exception
91+
"""
92+
invalid_object = "invalid"
93+
filename = "/tmp/invalid.pkl"
94+
with pytest.raises(TypeError):
95+
save(invalid_object, filename)
96+
97+
98+
def test_save_with_invalid_protocol(
99+
detector: BaseDetector,
100+
) -> None:
101+
"""Test save with invalid protocol.
102+
103+
:param detector: Detector
104+
:type detector: BaseDetector
105+
:raises ValueError: Value error exception
106+
"""
107+
filename = "/tmp/detector.pkl"
108+
invalid_protocol = pickle.HIGHEST_PROTOCOL + 1
109+
with pytest.raises(ValueError):
110+
save(detector, filename, invalid_protocol)
111+
112+
113+
def test_load_with_non_existent_file() -> None:
114+
"""Test load with non-existent file.
115+
116+
:raises FileNotFoundError: File not found error exception
117+
"""
118+
filename = "/tmp/non_existent.pkl"
119+
with pytest.raises(FileNotFoundError):
120+
load(filename)

frouros/utils/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
11
"""Utils init."""
2+
3+
from .persistence import load, save
4+
5+
__all__ = [
6+
"load",
7+
"save",
8+
]

frouros/utils/persistence.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""Persistence module."""
2+
3+
import pickle
4+
5+
from frouros.callbacks.base import BaseCallback
6+
from frouros.detectors.base import BaseDetector
7+
from frouros.utils.logger import logger
8+
9+
DEFAULT_PROTOCOL = pickle.DEFAULT_PROTOCOL
10+
11+
12+
def load(
13+
filename: str,
14+
) -> object:
15+
"""Load object from file.
16+
17+
:param filename: Filename
18+
:type filename: str
19+
:return: Loaded object
20+
:rtype: object
21+
"""
22+
try:
23+
with open(filename, "rb") as file:
24+
obj = pickle.load(
25+
file,
26+
)
27+
return obj
28+
except (IOError, pickle.UnpicklingError) as e:
29+
logger.error("Error occurred while loading object: %s", e)
30+
raise e
31+
32+
33+
def save(
34+
obj: object,
35+
filename: str,
36+
pickle_protocol: int = DEFAULT_PROTOCOL,
37+
) -> None:
38+
"""Save object to file.
39+
40+
:param obj: Object to save
41+
:type obj: object
42+
:param filename: Filename
43+
:type filename: str
44+
:param pickle_protocol: Pickle protocol, defaults to DEFAULT_PROTOCOL
45+
:type pickle_protocol: int, optional
46+
"""
47+
try:
48+
if not isinstance(obj, (BaseDetector, BaseCallback)):
49+
raise TypeError(
50+
f"Object of type {type(obj)} is not serializable. "
51+
f"Must be an instance that inherits from BaseDetector or BaseCallback."
52+
)
53+
if pickle_protocol not in range(pickle.HIGHEST_PROTOCOL + 1):
54+
raise ValueError(
55+
f"Invalid pickle_protocol value. "
56+
f"Must be in range 0..{pickle.HIGHEST_PROTOCOL}."
57+
)
58+
with open(filename, "wb") as file:
59+
pickle.dump(
60+
obj,
61+
file,
62+
protocol=pickle_protocol,
63+
)
64+
except (IOError, pickle.PicklingError) as e:
65+
logger.error("Error occurred while saving object: %s", e)
66+
raise e

0 commit comments

Comments
 (0)