skfolio 0.0.1__py3-none-any.whl

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 (79) hide show
  1. skfolio/__init__.py +29 -0
  2. skfolio/cluster/__init__.py +8 -0
  3. skfolio/cluster/_hierarchical.py +387 -0
  4. skfolio/datasets/__init__.py +20 -0
  5. skfolio/datasets/_base.py +389 -0
  6. skfolio/datasets/data/__init__.py +0 -0
  7. skfolio/datasets/data/factors_dataset.csv.gz +0 -0
  8. skfolio/datasets/data/sp500_dataset.csv.gz +0 -0
  9. skfolio/datasets/data/sp500_index.csv.gz +0 -0
  10. skfolio/distance/__init__.py +26 -0
  11. skfolio/distance/_base.py +55 -0
  12. skfolio/distance/_distance.py +574 -0
  13. skfolio/exceptions.py +30 -0
  14. skfolio/measures/__init__.py +76 -0
  15. skfolio/measures/_enums.py +355 -0
  16. skfolio/measures/_measures.py +607 -0
  17. skfolio/metrics/__init__.py +3 -0
  18. skfolio/metrics/_scorer.py +121 -0
  19. skfolio/model_selection/__init__.py +18 -0
  20. skfolio/model_selection/_combinatorial.py +407 -0
  21. skfolio/model_selection/_validation.py +194 -0
  22. skfolio/model_selection/_walk_forward.py +221 -0
  23. skfolio/moments/__init__.py +41 -0
  24. skfolio/moments/covariance/__init__.py +29 -0
  25. skfolio/moments/covariance/_base.py +101 -0
  26. skfolio/moments/covariance/_covariance.py +1108 -0
  27. skfolio/moments/expected_returns/__init__.py +21 -0
  28. skfolio/moments/expected_returns/_base.py +31 -0
  29. skfolio/moments/expected_returns/_expected_returns.py +415 -0
  30. skfolio/optimization/__init__.py +36 -0
  31. skfolio/optimization/_base.py +147 -0
  32. skfolio/optimization/cluster/__init__.py +13 -0
  33. skfolio/optimization/cluster/_nco.py +348 -0
  34. skfolio/optimization/cluster/hierarchical/__init__.py +13 -0
  35. skfolio/optimization/cluster/hierarchical/_base.py +440 -0
  36. skfolio/optimization/cluster/hierarchical/_herc.py +406 -0
  37. skfolio/optimization/cluster/hierarchical/_hrp.py +368 -0
  38. skfolio/optimization/convex/__init__.py +16 -0
  39. skfolio/optimization/convex/_base.py +1944 -0
  40. skfolio/optimization/convex/_distributionally_robust.py +392 -0
  41. skfolio/optimization/convex/_maximum_diversification.py +417 -0
  42. skfolio/optimization/convex/_mean_risk.py +974 -0
  43. skfolio/optimization/convex/_risk_budgeting.py +560 -0
  44. skfolio/optimization/ensemble/__init__.py +6 -0
  45. skfolio/optimization/ensemble/_base.py +87 -0
  46. skfolio/optimization/ensemble/_stacking.py +326 -0
  47. skfolio/optimization/naive/__init__.py +3 -0
  48. skfolio/optimization/naive/_naive.py +173 -0
  49. skfolio/population/__init__.py +3 -0
  50. skfolio/population/_population.py +883 -0
  51. skfolio/portfolio/__init__.py +13 -0
  52. skfolio/portfolio/_base.py +1096 -0
  53. skfolio/portfolio/_multi_period_portfolio.py +610 -0
  54. skfolio/portfolio/_portfolio.py +842 -0
  55. skfolio/pre_selection/__init__.py +7 -0
  56. skfolio/pre_selection/_pre_selection.py +342 -0
  57. skfolio/preprocessing/__init__.py +3 -0
  58. skfolio/preprocessing/_returns.py +114 -0
  59. skfolio/prior/__init__.py +18 -0
  60. skfolio/prior/_base.py +63 -0
  61. skfolio/prior/_black_litterman.py +238 -0
  62. skfolio/prior/_empirical.py +163 -0
  63. skfolio/prior/_factor_model.py +268 -0
  64. skfolio/typing.py +50 -0
  65. skfolio/uncertainty_set/__init__.py +23 -0
  66. skfolio/uncertainty_set/_base.py +108 -0
  67. skfolio/uncertainty_set/_bootstrap.py +281 -0
  68. skfolio/uncertainty_set/_empirical.py +237 -0
  69. skfolio/utils/__init__.py +0 -0
  70. skfolio/utils/bootstrap.py +115 -0
  71. skfolio/utils/equations.py +350 -0
  72. skfolio/utils/sorting.py +117 -0
  73. skfolio/utils/stats.py +466 -0
  74. skfolio/utils/tools.py +567 -0
  75. skfolio-0.0.1.dist-info/LICENSE +29 -0
  76. skfolio-0.0.1.dist-info/METADATA +568 -0
  77. skfolio-0.0.1.dist-info/RECORD +79 -0
  78. skfolio-0.0.1.dist-info/WHEEL +5 -0
  79. skfolio-0.0.1.dist-info/top_level.txt +1 -0
skfolio/utils/tools.py ADDED
@@ -0,0 +1,567 @@
1
+ """Tools module"""
2
+
3
+ # Author: Hugo Delatte <delatte.hugo@gmail.com>
4
+ # License: BSD 3 clause
5
+
6
+ from collections.abc import Callable, Iterator
7
+ from enum import Enum
8
+ from functools import wraps
9
+
10
+ import numpy as np
11
+ import numpy.typing as npt
12
+ import pandas as pd
13
+ import sklearn as sk
14
+ import sklearn.base as skb
15
+
16
+ __all__ = [
17
+ "AutoEnum",
18
+ "cached_property_slots",
19
+ "cache_method",
20
+ "input_to_array",
21
+ "args_names",
22
+ "format_measure",
23
+ "bisection",
24
+ "safe_split",
25
+ "fit_single_estimator",
26
+ "fit_and_predict",
27
+ "deduplicate_names",
28
+ "default_asset_names",
29
+ "check_estimator",
30
+ ]
31
+
32
+ GenericAlias = type(list[int])
33
+
34
+
35
+ class AutoEnum(str, Enum):
36
+ """Base Enum class used in `skfolio`"""
37
+
38
+ @staticmethod
39
+ def _generate_next_value_(
40
+ name: str, start: int, count: int, last_values: any
41
+ ) -> str:
42
+ """Overriding `auto()`"""
43
+ return name.lower()
44
+
45
+ @classmethod
46
+ def has(cls, value: str) -> bool:
47
+ """Check if a value is in the Enum.
48
+
49
+ Parameters
50
+ ----------
51
+ value : str
52
+ Input value.
53
+
54
+ Returns
55
+ -------
56
+ x : bool
57
+ True if the value is in the Enum, False otherwise.
58
+ """
59
+ return value in cls._value2member_map_
60
+
61
+ def __repr__(self) -> str:
62
+ """Representation of the Enum"""
63
+ return self.name
64
+
65
+
66
+ # noinspection PyPep8Naming
67
+ class cached_property_slots:
68
+ """Cached property decorator for slots"""
69
+
70
+ def __init__(self, func):
71
+ self.func = func
72
+ self.public_name = None
73
+ self.private_name = None
74
+ self.__doc__ = func.__doc__
75
+
76
+ def __set_name__(self, owner, name):
77
+ self.public_name = name
78
+ self.private_name = f"_{name}"
79
+
80
+ def __get__(self, instance, owner=None):
81
+ if instance is None:
82
+ return self
83
+ if self.private_name is None:
84
+ raise TypeError(
85
+ "Cannot use cached_property instance without calling __set_name__"
86
+ " on it."
87
+ )
88
+ try:
89
+ value = getattr(instance, self.private_name)
90
+ except AttributeError:
91
+ value = self.func(instance)
92
+ setattr(instance, self.private_name, value)
93
+ return value
94
+
95
+ def __set__(self, instance, owner=None):
96
+ raise AttributeError(
97
+ f"'{type(instance).__name__}' object attribute '{self.public_name}' is"
98
+ " read-only"
99
+ )
100
+
101
+ __class_getitem__ = classmethod(GenericAlias)
102
+
103
+
104
+ def _make_key(args, kwds) -> int:
105
+ """Make a cache key from optionally typed positional and keyword arguments"""
106
+ key = args
107
+ if kwds:
108
+ for item in kwds.items():
109
+ key += item
110
+ return hash(key)
111
+
112
+
113
+ def cache_method(cache_name: str) -> Callable:
114
+ """Decorator that caches class methods results into a class dictionary.
115
+
116
+ Parameters
117
+ ----------
118
+ cache_name : str
119
+ Name of the dictionary class attribute.
120
+
121
+ Returns
122
+ -------
123
+ func : Callable
124
+ Decorating function that caches class methods.
125
+ """
126
+
127
+ # To avoid memory leakage and proper garbage collection, self should not be part of
128
+ # the cache key.
129
+ # This is a known issue when we use functools.lru_cache on class methods.
130
+ def decorating_function(method):
131
+ @wraps(method)
132
+ def wrapper(self, *args, **kwargs):
133
+ func_name = method.__name__
134
+ key = _make_key(args, kwargs)
135
+ try:
136
+ cache = getattr(self, cache_name)
137
+ except AttributeError:
138
+ raise AttributeError(
139
+ "You first need to create a dictionary class attribute named "
140
+ f"'{cache_name}'"
141
+ ) from None
142
+ if not isinstance(cache, dict):
143
+ raise AttributeError(
144
+ f"'The cache named '{cache_name}' must be a "
145
+ f"dictionary, got {type(cache)}"
146
+ )
147
+ if func_name not in cache:
148
+ cache[func_name] = {}
149
+ c = cache[func_name]
150
+ if key not in c:
151
+ c[key] = method(self, *args, **kwargs)
152
+ return c[key]
153
+
154
+ return wrapper
155
+
156
+ return decorating_function
157
+
158
+
159
+ def args_names(func: object) -> list[str]:
160
+ """Returns the argument names of a function.
161
+
162
+ Parameters
163
+ ----------
164
+ func : object
165
+ Function.
166
+
167
+ Returns
168
+ -------
169
+ args : list[str]
170
+ The list of function arguments.
171
+ """
172
+ return [
173
+ v for v in func.__code__.co_varnames[: func.__code__.co_argcount] if v != "self"
174
+ ]
175
+
176
+
177
+ def check_estimator(
178
+ estimator: skb.BaseEstimator | None, default: skb.BaseEstimator, check_type: any
179
+ ):
180
+ """Check the estimator type and returns its cloned version it provided, otherwise
181
+ return the default estimator.
182
+
183
+ Parameters
184
+ ----------
185
+ estimator : BaseEstimator, optional
186
+ Estimator.
187
+
188
+ default : BaseEstimator
189
+ Default estimator to return when `estimator` is `None`.
190
+
191
+ check_type : any
192
+ Expected type of the estimator to check against.
193
+
194
+ Returns
195
+ -------
196
+ estimator: Estimator
197
+ The checked estimator or the default.
198
+ """
199
+
200
+ if estimator is None:
201
+ return default
202
+ if not isinstance(estimator, check_type):
203
+ raise TypeError(f"Expected type {check_type}, got {type(estimator)}")
204
+ return sk.clone(estimator)
205
+
206
+
207
+ def input_to_array(
208
+ items: dict | npt.ArrayLike,
209
+ n_assets: int,
210
+ fill_value: any,
211
+ dim: int,
212
+ assets_names: np.ndarray | None,
213
+ name: str,
214
+ ) -> np.ndarray:
215
+ """Convert a collection of items (array-like or dictionary) into
216
+ a numpy array and verify its shape.
217
+
218
+ Parameters
219
+ ----------
220
+ items : np.ndarray | dict | list
221
+ Items to verify and convert to array.
222
+
223
+ n_assets : int
224
+ Expected number of assets.
225
+ Used to verify the shape of the converted array.
226
+
227
+ fill_value : any
228
+ When `items` is a dictionary, elements that are not in `asset_names` are filled
229
+ with `fill_value` in the converted array.
230
+
231
+ dim : int
232
+ Dimension of the final array.
233
+ Possible values are `1` or `2`.
234
+
235
+ assets_names : ndarray, optional
236
+ Asset names used when `items` is a dictionary.
237
+
238
+ name : str
239
+ Name of the items used for error messages.
240
+
241
+ Returns
242
+ -------
243
+ values : ndarray of shape (n_assets) for dim=1 or (n_groups, n_assets) for dim=2
244
+ Converted array.
245
+ """
246
+ if dim not in [1, 2]:
247
+ raise ValueError(f"dim must be 1 or 2, got {dim}")
248
+ if isinstance(items, dict):
249
+ if assets_names is None:
250
+ raise ValueError(
251
+ f"If `{name}` is provided as a dictionary, you must input `X` as a"
252
+ " DataFrame with assets names in columns"
253
+ )
254
+ if dim == 1:
255
+ arr = np.array([items.get(asset, fill_value) for asset in assets_names])
256
+ else:
257
+ # add assets and convert dict to ordered array
258
+ arr = {}
259
+ for asset in assets_names:
260
+ elem = items.get(asset)
261
+ if elem is None:
262
+ elem = [asset]
263
+ elif np.isscalar(elem):
264
+ elem = [asset, elem]
265
+ else:
266
+ elem = [asset, *elem]
267
+ arr[asset] = elem
268
+ arr = (
269
+ pd.DataFrame.from_dict(arr, orient="index")
270
+ .loc[assets_names]
271
+ .to_numpy()
272
+ .T
273
+ )
274
+ else:
275
+ arr = np.asarray(items)
276
+
277
+ if arr.ndim != dim:
278
+ raise ValueError(f"`{name}` must be a {dim}D array, got a {arr.ndim}D array")
279
+
280
+ if not isinstance(fill_value, str) and np.isnan(arr).any():
281
+ raise ValueError(f"`{name}` contains NaN")
282
+
283
+ if arr.shape[-1] != n_assets:
284
+ if dim == 1:
285
+ s = "(n_assets,)"
286
+ else:
287
+ s = "(n_groups, n_assets)"
288
+ raise ValueError(
289
+ f"`{name}` must be a of shape {s} with n_assets={n_assets}, "
290
+ f"got {arr.shape[0]}"
291
+ )
292
+ return arr
293
+
294
+
295
+ def format_measure(x: float, percent: bool = False) -> str:
296
+ """Format a measure number into a user-friendly string.
297
+
298
+ Parameters
299
+ ----------
300
+ x : float
301
+ Number to format.
302
+
303
+ percent : bool, default=False
304
+ If this is set to True, the number is formatted in percentage.
305
+
306
+ Returns
307
+ -------
308
+ formatted : str
309
+ Formatted string.
310
+ """
311
+ if np.isnan(x):
312
+ return str(x)
313
+ if percent:
314
+ xn = x * 100
315
+ f = "%"
316
+ else:
317
+ xn = x
318
+ f = "f"
319
+ if xn == 0:
320
+ n = 0
321
+ else:
322
+ n = min(6, max(int(-np.log10(abs(xn))) + 2, 2))
323
+ return "{value:{fmt}}".format(value=x, fmt=f".{n}{f}")
324
+
325
+
326
+ def bisection(x: list[np.ndarray]) -> Iterator[list[np.ndarray, np.ndarray]]:
327
+ """Generator to bisect a list of array.
328
+
329
+ Parameters
330
+ ----------
331
+ x : list[ndarray]
332
+ A list of array.
333
+
334
+ Yields
335
+ ------
336
+ arr : Iterator[list[ndarray, ndarray]]
337
+ Bisected array.
338
+ """
339
+ for e in x:
340
+ n = len(e)
341
+ if n > 1:
342
+ mid = n // 2
343
+ yield [e[0:mid], e[mid:n]]
344
+
345
+
346
+ def safe_indexing(
347
+ X: npt.ArrayLike | pd.DataFrame, indices: npt.ArrayLike | None, axis: int = 0
348
+ ):
349
+ """
350
+ Return rows, items or columns of X using indices.
351
+
352
+ Parameters
353
+ ----------
354
+ X : array-like
355
+ Data from which to sample rows.
356
+
357
+ indices : array-like, optional
358
+ Indices of rows or columns.
359
+ The default (`None`) is to select the entire data.
360
+
361
+ axis : int, default=0
362
+ The axis along which `X` will be sub-sampled. `axis=0` will select
363
+ rows while `axis=1` will select columns.
364
+
365
+ Returns
366
+ -------
367
+ subset :
368
+ Subset of X on axis 0.
369
+ """
370
+ if indices is None:
371
+ return X
372
+ if hasattr(X, "iloc"):
373
+ return X.take(indices, axis=axis)
374
+ if axis == 0:
375
+ return X[indices]
376
+ return X[:, indices]
377
+
378
+
379
+ def safe_split(
380
+ X: npt.ArrayLike,
381
+ y: npt.ArrayLike | None = None,
382
+ indices: np.ndarray | None = None,
383
+ axis: int = 0,
384
+ ):
385
+ """Create subset of dataset.
386
+
387
+ Slice X, y according to indices for cross-validation.
388
+
389
+ Parameters
390
+ ----------
391
+ X : array-like
392
+ Data to be indexed.
393
+
394
+ y : array-like
395
+ Data to be indexed.
396
+
397
+ indices : ndarray of int, optional
398
+ Rows or columns to select from X and y.
399
+ The default (`None`) is to select the entire data.
400
+
401
+ axis : int, default=0
402
+ The axis along which `X` will be sub-sampled. `axis=0` will select
403
+ rows while `axis=1` will select columns.
404
+
405
+ Returns
406
+ -------
407
+ X_subset : array-like
408
+ Indexed data.
409
+
410
+ y_subset : array-like
411
+ Indexed targets.
412
+ """
413
+
414
+ X_subset = safe_indexing(X, indices=indices, axis=axis)
415
+ if y is not None:
416
+ y_subset = safe_indexing(y, indices=indices, axis=axis)
417
+ else:
418
+ y_subset = None
419
+ return X_subset, y_subset
420
+
421
+
422
+ def fit_single_estimator(
423
+ estimator: any,
424
+ X: npt.ArrayLike,
425
+ y: npt.ArrayLike | None = None,
426
+ indices: np.ndarray | None = None,
427
+ axis: int = 0,
428
+ ):
429
+ """function used to fit an estimator within a job.
430
+
431
+ Parameters
432
+ ----------
433
+ estimator : estimator object implementing 'fit' and 'predict'
434
+ The object to use to fit the data.
435
+
436
+ X : array-like of shape (n_observations, n_assets)
437
+ The data to fit.
438
+
439
+ y : array-like of shape (n_observations, n_targets), optional
440
+ The target array if provided.
441
+
442
+ indices : ndarray of int, optional
443
+ Rows or columns to select from X and y.
444
+ The default (`None`) is to select the entire data.
445
+
446
+ axis : int, default=0
447
+ The axis along which `X` will be sub-sampled. `axis=0` will select
448
+ rows while `axis=1` will select columns.
449
+
450
+ Returns
451
+ -------
452
+ fitted_estimator : estimator
453
+ The fitted estimator.
454
+ """
455
+
456
+ X, y = safe_split(X, y, indices=indices, axis=axis)
457
+ estimator.fit(X, y)
458
+ return estimator
459
+
460
+
461
+ def fit_and_predict(
462
+ estimator: any,
463
+ X: npt.ArrayLike,
464
+ y: npt.ArrayLike | None,
465
+ train: np.ndarray,
466
+ test: np.ndarray | list[np.ndarray],
467
+ fit_params: dict,
468
+ method: str,
469
+ column_indices: np.ndarray | None = None,
470
+ ) -> npt.ArrayLike | list[npt.ArrayLike]:
471
+ """Fit the estimator and predict values for a given dataset split.
472
+
473
+ Parameters
474
+ ----------
475
+ estimator : estimator object implementing 'fit' and 'predict'
476
+ The object to use to fit the data.
477
+
478
+ X : array-like of shape (n_observations, n_assets)
479
+ The data to fit.
480
+
481
+ y : array-like of shape (n_observations, n_factors) or None
482
+ The factor array if provided
483
+
484
+ train : ndarray of int of shape (n_train_observations,)
485
+ Indices of training samples.
486
+
487
+ test : ndarray of int of shape (n_test_samples,) or list of ndarray
488
+ Indices of test samples or list of indices.
489
+
490
+ fit_params : dict
491
+ Parameters that will be passed to ``estimator.fit``.
492
+
493
+ method : str
494
+ Invokes the passed method name of the passed estimator.
495
+
496
+ column_indices : ndarray, optional
497
+ Indices of columns to select.
498
+ The default (`None`) is to select all columns.
499
+
500
+ Returns
501
+ -------
502
+ predictions : array-like or list of array-like
503
+ If `test` is an array, it returns the array-like result of calling
504
+ 'estimator.method' on `test`.
505
+ Otherwise, if `test` is a list of arrays, it returns the list of array-like
506
+ results of calling 'estimator.method' on each test set in `test`.
507
+ """
508
+ fit_params = fit_params if fit_params is not None else {}
509
+ X, y = safe_split(X, y, indices=column_indices, axis=1)
510
+ X_train, y_train = safe_split(X, y, indices=train, axis=0)
511
+ if y_train is None:
512
+ estimator.fit(X_train, **fit_params)
513
+ else:
514
+ estimator.fit(X_train, y_train, **fit_params)
515
+ func = getattr(estimator, method)
516
+
517
+ if isinstance(test, list):
518
+ predictions = []
519
+ for t in test:
520
+ X_test, _ = safe_split(X, indices=t, axis=0)
521
+ predictions.append(func(X_test))
522
+ else:
523
+ X_test, _ = safe_split(X, indices=test, axis=0)
524
+ predictions = func(X_test)
525
+
526
+ return predictions
527
+
528
+
529
+ def default_asset_names(n_assets: int) -> np.ndarray:
530
+ """Default asset names are `["x0", "x1", ..., "x(n_assets - 1)"]`
531
+
532
+ Parameters
533
+ ----------
534
+ n_assets : int
535
+ Number of assets.
536
+
537
+ Returns
538
+ -------
539
+ asset_names : ndarray of str
540
+ Default assets names.
541
+ """
542
+ return np.asarray([f"x{i}" for i in range(n_assets)], dtype=object)
543
+
544
+
545
+ def deduplicate_names(names: npt.ArrayLike) -> list[str]:
546
+ """Rename duplicated names by appending "_{duplicate_nb}" at the end.
547
+
548
+ This function is inspired by the pandas function `_maybe_dedup_names`.
549
+
550
+ Parameters
551
+ ----------
552
+ names : array-like of shape (n_names,)
553
+ List of names.
554
+
555
+ Returns
556
+ -------
557
+ names : list[str]
558
+ Deduplicate names.
559
+ """
560
+ names = list(names)
561
+ counts = {}
562
+ for i, col in enumerate(names):
563
+ cur_count = counts.get(col, 0)
564
+ if cur_count > 0:
565
+ names[i] = f"{col}_{cur_count}"
566
+ counts[col] = cur_count + 1
567
+ return names
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2007-2023 The skfolio developers.
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ * Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ * Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ * Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.