Skip to content

Commit 0ef6865

Browse files
committed
Move decorators to their own submodule
1 parent 9e0ec7a commit 0ef6865

11 files changed

Lines changed: 435 additions & 425 deletions

File tree

notebooks/50_gravity-inversion-with-model-slice.ipynb

Lines changed: 152 additions & 152 deletions
Large diffs are not rendered by default.

notebooks/regressor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from scipy.sparse.linalg import LinearOperator
1010

1111
from inversion_ideas.base import Simulation
12-
from inversion_ideas.utils import cache_on_model
12+
from inversion_ideas.decorators import cache_on_model
1313

1414

1515
class LinearRegressor(Simulation):

src/inversion_ideas/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Ideas for inversion framework.
33
"""
44

5-
from . import base, operators, typing, utils
5+
from . import base, decorators, operators, typing, utils
66
from ._version import __version__
77
from .conditions import ChiTarget, CustomCondition, ModelChanged, ObjectiveChanged
88
from .data_misfit import DataMisfit
@@ -56,6 +56,7 @@
5656
"create_l2_inversion",
5757
"create_sparse_inversion",
5858
"create_tikhonov_regularization",
59+
"decorators",
5960
"get_jacobi_preconditioner",
6061
"operators",
6162
"typing",

src/inversion_ideas/data_misfit.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
from inversion_ideas.utils import get_logger
1111

1212
from .base import Objective
13+
from .decorators import expand_output, slice_model
1314
from .operators import get_diagonal
1415
from .typing import Model, SparseArray
15-
from .utils import expand_output, slice_model
1616
from .wires import ModelSlice, MultiSlice
1717

1818

src/inversion_ideas/decorators.py

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
"""
2+
Decorators for functions, methods and classes.
3+
"""
4+
5+
import functools
6+
import hashlib
7+
8+
from ._utils import array_to_str
9+
from .utils import get_logger
10+
from .wires import ModelSlice, MultiSlice
11+
12+
13+
def slice_model(func):
14+
"""
15+
Slice the input model applying the ``model_slice`` attribute in the instance.
16+
17+
Use this decorator on methods that take ``model`` as the first parameter. The
18+
decorator will use the ``model_slice`` attribute of the object to extract the
19+
relevant slice of the model and pass it to the decorated method.
20+
21+
.. important::
22+
23+
Use this decorator only for methods that take the ``model`` as the first
24+
argument.
25+
26+
.. important::
27+
28+
The instance needs to have a ``model_slice`` attribute. If it's None, then the
29+
method will run without any modification.
30+
"""
31+
32+
@functools.wraps(func)
33+
def wrapper(self, model, *args, **kwargs):
34+
if not hasattr(self, "model_slice"):
35+
msg = (
36+
f"Object '{self}' doesn't have a 'model_slice' attribute. "
37+
"Cannot slice the model without it."
38+
)
39+
raise AttributeError(msg)
40+
41+
# Get model slice
42+
model_slice: ModelSlice | MultiSlice = self.model_slice
43+
44+
# Don't slice the model if the object has model_slice as None
45+
if model_slice is None:
46+
return func(self, model, *args, **kwargs)
47+
48+
# Don't slice the model if it's already reduced
49+
if model.size != model_slice.full_size:
50+
if model.size != model_slice.size:
51+
msg = (
52+
f"Invalid model of size '{model.size}'. "
53+
f"It should be the full model "
54+
f"(size of {model_slice.full_size}) "
55+
f"or the reduced model (size of {model_slice.size})."
56+
)
57+
raise ValueError(msg)
58+
return func(self, model, *args, **kwargs)
59+
60+
# Slice the model and call the method with it
61+
model_reduced = model_slice.extract(model)
62+
return func(self, model_reduced, *args, **kwargs)
63+
64+
return wrapper
65+
66+
67+
def expand_output(func):
68+
"""
69+
Expand output of method using the ``model_slice`` attribute in the instance.
70+
71+
Use this decorator on any method that needs to expand its output based on the
72+
``model_slice`` attribute of the instance.
73+
If it's a 1D array, it'll expand the array to fill the extra elements with zeros.
74+
If it's a square matrix or a square linear operator, it'll fill the extra blocks
75+
with zeros.
76+
77+
.. important::
78+
79+
The instance needs to have a ``model_slice`` attribute. If it's None, then the
80+
method will run without any modification.
81+
82+
.. important::
83+
84+
Use this decorator only for methods that return a 1D array, or a 2D array,
85+
sparse matrix or ``LinearOperator``.
86+
87+
"""
88+
89+
@functools.wraps(func)
90+
def wrapper(self, model, *args, **kwargs):
91+
if not hasattr(self, "model_slice"):
92+
msg = (
93+
f"Object '{self}' doesn't have a 'model_slice' attribute. "
94+
f"Cannot expand the output of '{func}' without it."
95+
)
96+
raise AttributeError(msg)
97+
98+
# Get model slice
99+
model_slice: ModelSlice | MultiSlice = self.model_slice
100+
101+
# Don't modify the output if the object has no model_slice
102+
if model_slice is None:
103+
return func(self, model, *args, **kwargs)
104+
105+
result = func(self, model, *args, **kwargs)
106+
107+
if not hasattr(result, "ndim"):
108+
msg = (
109+
f"Invalid object '{result}' of type '{type(result).__name__}' "
110+
f"returned by '{func}''. "
111+
"The output of the method should be an array, sparse matrix or "
112+
"LinearOperator to be able to expand it."
113+
)
114+
raise TypeError(msg)
115+
116+
if result.ndim == 1:
117+
result = model_slice.expand_array(result)
118+
elif result.ndim == 2:
119+
result = model_slice.expand_matrix(result)
120+
else:
121+
msg = (
122+
f"Invalid object with {result.ndim} dimensions. "
123+
"It must be a 1D or 2D array-like object to be able to expand it."
124+
)
125+
raise ValueError(msg)
126+
return result
127+
128+
return wrapper
129+
130+
131+
def cache_on_model(func):
132+
"""
133+
Cache the last result of a method within the instance using the model hash.
134+
135+
.. important::
136+
137+
Use this decorator only for methods that take the ``model`` as the first
138+
argument.
139+
140+
.. important::
141+
142+
The instance needs to have a ``cache`` bool attribute. If True, the result
143+
of the decorated method will be cached. If False, no caching will be performed.
144+
145+
Examples
146+
--------
147+
>>> import numpy as np
148+
>>>
149+
>>> class MyClass:
150+
...
151+
... def __init__(self):
152+
... self.cache = True
153+
...
154+
... @cache_on_model
155+
... def squared(self, model) -> float:
156+
... return (model ** 2).sum()
157+
>>>
158+
>>> sq = MyClass()
159+
>>> model = np.array([1.0, 2.0, 3.0])
160+
>>> print(sq.squared(model)) # perform the computation
161+
14.0
162+
>>> print(sq.squared(model)) # access the cached result
163+
14.0
164+
165+
>>> model_new = np.array([4.0, 5.0, 6.0])
166+
>>> print(sq.squared(model_new)) # perform a new computation
167+
77.0
168+
"""
169+
# Define attribute name for the cached result using the hash of the function
170+
cache_attr = f"_cache_{hash(func)}"
171+
172+
@functools.wraps(func)
173+
def wrapper(self, model, *args, **kwargs):
174+
if not hasattr(self, "cache"):
175+
msg = f"Missing 'cache' attribute in {self}"
176+
raise AttributeError(msg)
177+
178+
if self.cache:
179+
model_hash = hashlib.sha256(model)
180+
181+
# Return cached object if the model hash matches with the cached one
182+
if hasattr(self, cache_attr):
183+
model_hash_cached, cached_result = getattr(self, cache_attr)
184+
if model_hash_cached.digest() == model_hash.digest():
185+
# -- Debug log --
186+
msg = (
187+
f"Returning cached object '{array_to_str(cached_result)}' "
188+
f"after calling '{func}' with model with hash "
189+
f"'{model_hash_cached.hexdigest()}'."
190+
)
191+
if args:
192+
msg += f" With args: '{args}'."
193+
if kwargs:
194+
msg += f" With kwargs: '{kwargs}'."
195+
get_logger().debug(msg)
196+
# ---
197+
return cached_result
198+
199+
# Compute new result and cache it
200+
result = func(self, model, *args, **kwargs)
201+
setattr(self, cache_attr, (model_hash, result))
202+
# -- Debug log --
203+
msg = (
204+
f"Computed new result '{array_to_str(result)}' after "
205+
f"calling '{func}' with model with hash '{model_hash.hexdigest()}'. "
206+
"Cached the result into the object."
207+
)
208+
if args:
209+
msg += f" With args: '{args}'."
210+
if kwargs:
211+
msg += f" With kwargs: '{kwargs}'."
212+
get_logger().debug(msg)
213+
# ---
214+
return result
215+
216+
# Return result without caching
217+
return func(self, model, *args, **kwargs)
218+
219+
return wrapper
220+
221+
222+
def debug(func):
223+
"""
224+
Add a debug entry into the logger through a decorator.
225+
226+
Use this decorator on methods and functions. When such method or function gets
227+
called, it will add an entry into the logger as a DEBUG level.
228+
"""
229+
logger = get_logger()
230+
231+
@functools.wraps(func)
232+
def wrapper(self, *args, **kwargs):
233+
msg = f"Called '{func}'"
234+
if args:
235+
msg += f" with arguments '{args}'"
236+
if kwargs:
237+
msg += f" with keyword arguments '{kwargs}'"
238+
msg += "."
239+
logger.debug(msg)
240+
return func(self, *args, **kwargs)
241+
242+
return wrapper
243+
244+
245+
class CountCalls:
246+
"""
247+
Class decorator to count function calls.
248+
249+
Examples
250+
--------
251+
>>> @CountCalls
252+
... def my_function(x):
253+
... return x**2
254+
>>> my_function(1)
255+
1
256+
>>> my_function(2)
257+
4
258+
>>> my_function.counts
259+
2
260+
"""
261+
262+
def __init__(self, func):
263+
functools.update_wrapper(self, func)
264+
self.func = func
265+
self.counts = 0
266+
267+
def __call__(self, *args, **kwargs):
268+
result = self.func(*args, **kwargs)
269+
self.counts += 1
270+
return result

src/inversion_ideas/inversion.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818

1919
from inversion_ideas.errors import ConvergenceWarning
2020

21+
from ._utils import array_to_str
2122
from .base import Condition, Directive, Minimizer, Objective
23+
from .decorators import debug
2224
from .inversion_log import InversionLog, InversionLogRich, MinimizerLog
2325
from .typing import Log, Model
24-
from .utils import array_to_str, debug, get_logger
26+
from .utils import get_logger
2527

2628

2729
class Inversion:

src/inversion_ideas/minimize/_minimizers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
from scipy.sparse.linalg import cg
1111

1212
from ..base import Condition, Minimizer, MinimizerResult, Objective
13+
from ..decorators import CountCalls
1314
from ..errors import ConvergenceWarning
1415
from ..typing import CanBeUpdated, Model, Preconditioner
15-
from ..utils import CountCalls, Counter, get_logger
16+
from ..utils import Counter, get_logger
1617
from ._utils import backtracking_line_search
1718

1819

src/inversion_ideas/regularization/_mesh_based.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717

1818
from .._utils import prod_arrays
1919
from ..base import Objective
20+
from ..decorators import expand_output, slice_model
2021
from ..typing import Model
21-
from ..utils import expand_output, slice_model
2222
from ..wires import ModelSlice, MultiSlice
2323

2424

src/inversion_ideas/simulations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
from scipy.sparse.linalg import LinearOperator
88

99
from .base import Simulation
10+
from .decorators import cache_on_model
1011
from .typing import Model
11-
from .utils import cache_on_model
1212

1313

1414
def wrap_simulation(simulation, *, store_jacobian=False):

0 commit comments

Comments
 (0)