mmhdc 0.1.0__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.
- mmhdc/__init__.py +3 -0
- mmhdc/_cpp_backend.py +22 -0
- mmhdc/cpp/mmhdc.cpp +51 -0
- mmhdc/model.py +110 -0
- mmhdc/utils/__init__.py +3 -0
- mmhdc/utils/transform.py +67 -0
- mmhdc-0.1.0.dist-info/METADATA +74 -0
- mmhdc-0.1.0.dist-info/RECORD +11 -0
- mmhdc-0.1.0.dist-info/WHEEL +5 -0
- mmhdc-0.1.0.dist-info/licenses/LICENSE +21 -0
- mmhdc-0.1.0.dist-info/top_level.txt +1 -0
mmhdc/__init__.py
ADDED
mmhdc/_cpp_backend.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from functools import lru_cache
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@lru_cache(maxsize=1)
|
|
6
|
+
def get_mmhdc_cpp():
|
|
7
|
+
try:
|
|
8
|
+
from torch.utils.cpp_extension import load
|
|
9
|
+
|
|
10
|
+
return load(
|
|
11
|
+
name="mmhdc_cpp",
|
|
12
|
+
extra_cflags=["-O3"],
|
|
13
|
+
is_python_module=True,
|
|
14
|
+
sources=[str(Path(__file__).resolve().parent / "cpp" / "mmhdc.cpp")],
|
|
15
|
+
)
|
|
16
|
+
except Exception as exc:
|
|
17
|
+
raise RuntimeError(
|
|
18
|
+
"Failed to build or load the MM-HDC C++ backend. "
|
|
19
|
+
"The 'cpp' backend requires a working C++ toolchain and PyTorch C++ "
|
|
20
|
+
"extension build support. If this environment cannot compile the "
|
|
21
|
+
"extension, use backend='python' instead."
|
|
22
|
+
) from exc
|
mmhdc/cpp/mmhdc.cpp
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#include <torch/extension.h>
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Optimised step procedure — global GEMM reformulation:
|
|
5
|
+
|
|
6
|
+
All per-class loops are eliminated. The entire batch is handled with
|
|
7
|
+
exactly two GEMMs regardless of the number of classes K:
|
|
8
|
+
|
|
9
|
+
1. scores = x @ prototypes.T (N, K) — one GEMM
|
|
10
|
+
2. W built from the boolean margin-violation mask (N, K)
|
|
11
|
+
3. update = W.T @ x (K, D) — one GEMM
|
|
12
|
+
|
|
13
|
+
This maximises GPU utilisation (two large, dense kernels instead of K
|
|
14
|
+
small ones) and removes all GPU→CPU syncs (.item() calls).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
torch::Tensor step(torch::Tensor &x, torch::Tensor &y, torch::Tensor &prototypes, float lr, float C) {
|
|
18
|
+
// scores[i, k] = x_i · w_k for every sample and every class.
|
|
19
|
+
auto scores = torch::mm(x, prototypes.t()); // (N, K)
|
|
20
|
+
|
|
21
|
+
// correct_scores[i] = x_i · w_{y_i}
|
|
22
|
+
auto correct_scores = scores.gather(1, y.unsqueeze(1)); // (N, 1)
|
|
23
|
+
|
|
24
|
+
// margin_gap[i, k] = x_i · w_{y_i} - x_i · w_k
|
|
25
|
+
// = correct_score_i - score_{i,k}
|
|
26
|
+
// Margin is violated when gap < 2 (and k ≠ y_i).
|
|
27
|
+
auto violated = (correct_scores - scores) < 2; // (N, K) bool
|
|
28
|
+
|
|
29
|
+
// Zero out the diagonal: a sample cannot violate a margin against its own class.
|
|
30
|
+
violated.scatter_(1, y.unsqueeze(1),
|
|
31
|
+
torch::zeros_like(y.unsqueeze(1), torch::kBool));
|
|
32
|
+
|
|
33
|
+
// Build weight matrix W (N, K):
|
|
34
|
+
// W[i, k] = -1 if sample i violates margin against class k
|
|
35
|
+
// W[i, y_i] = +m_i where m_i is the number of violated margins for sample i
|
|
36
|
+
// (matches Python: each sample contributes once per violated class).
|
|
37
|
+
auto W = -violated.to(x.scalar_type()); // (N, K)
|
|
38
|
+
W.scatter_add_(1, y.unsqueeze(1),
|
|
39
|
+
violated.sum(1, /*keepdim=*/true).to(x.scalar_type()));
|
|
40
|
+
|
|
41
|
+
// prototypes_update[k] = Σ_i W[i,k] * x_i — single GEMM.
|
|
42
|
+
auto prototypes_update = torch::mm(W.t(), x); // (K, D)
|
|
43
|
+
|
|
44
|
+
// Regularised update.
|
|
45
|
+
prototypes = (1 - lr / C) * prototypes + lr * prototypes_update;
|
|
46
|
+
return prototypes;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
|
|
50
|
+
m.def("step", &step, "MM-HDC prototype update function");
|
|
51
|
+
}
|
mmhdc/model.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
from torch.nn.functional import one_hot, relu
|
|
3
|
+
from ._cpp_backend import get_mmhdc_cpp
|
|
4
|
+
|
|
5
|
+
class MultiMMHDC(torch.nn.Module):
|
|
6
|
+
def __init__(self, num_classes: int,
|
|
7
|
+
out_channels: int,
|
|
8
|
+
lr: float = 1e-2,
|
|
9
|
+
C: float = 1.0,
|
|
10
|
+
device: str = 'cpu',
|
|
11
|
+
backend: str = 'python',
|
|
12
|
+
dtype: torch.dtype = torch.float32):
|
|
13
|
+
|
|
14
|
+
super().__init__()
|
|
15
|
+
self.num_classes = num_classes
|
|
16
|
+
self.out_channels = out_channels
|
|
17
|
+
self.lr = lr
|
|
18
|
+
self.device = device
|
|
19
|
+
self.C = C
|
|
20
|
+
self.backend = backend
|
|
21
|
+
self.prototypes = torch.nn.parameter.Parameter(
|
|
22
|
+
data=torch.zeros(num_classes, out_channels, dtype=dtype, device=device),
|
|
23
|
+
requires_grad=False
|
|
24
|
+
)
|
|
25
|
+
self.dtype = dtype
|
|
26
|
+
|
|
27
|
+
def forward(self, x: torch.Tensor):
|
|
28
|
+
return torch.argmax(x @ self.prototypes.T, dim=1)
|
|
29
|
+
|
|
30
|
+
@torch.no_grad()
|
|
31
|
+
def initialize(self, x: torch.Tensor, y: torch.Tensor):
|
|
32
|
+
y = y.reshape(-1).to(dtype=torch.int64, device=x.device)
|
|
33
|
+
x = x.to(dtype=self.prototypes.dtype)
|
|
34
|
+
|
|
35
|
+
prototype_sums = torch.zeros(
|
|
36
|
+
self.num_classes,
|
|
37
|
+
self.out_channels,
|
|
38
|
+
dtype=self.prototypes.dtype,
|
|
39
|
+
device=x.device,
|
|
40
|
+
)
|
|
41
|
+
prototype_sums.index_add_(0, y, x)
|
|
42
|
+
|
|
43
|
+
class_counts = torch.bincount(y, minlength=self.num_classes)
|
|
44
|
+
class_counts = class_counts.unsqueeze(1).to(dtype=self.prototypes.dtype, device=x.device)
|
|
45
|
+
|
|
46
|
+
prototypes = prototype_sums / class_counts.clamp_min_(1)
|
|
47
|
+
prototype_norms = torch.norm(prototypes, dim=1, keepdim=True)
|
|
48
|
+
prototypes = prototypes / prototype_norms.clamp_min_(1e-8)
|
|
49
|
+
|
|
50
|
+
self.prototypes.copy_(prototypes.to(device=self.prototypes.device))
|
|
51
|
+
|
|
52
|
+
def loss(self, X: torch.Tensor, y: torch.Tensor):
|
|
53
|
+
y = y.reshape(-1)
|
|
54
|
+
scores = X @ self.prototypes.T
|
|
55
|
+
correct_scores = scores.gather(1, y.unsqueeze(1))
|
|
56
|
+
margins = relu(2 - (correct_scores - scores))
|
|
57
|
+
true_class_mask = one_hot(y, num_classes=self.num_classes).to(dtype=torch.bool)
|
|
58
|
+
margins = margins.masked_fill(true_class_mask, 0.0)
|
|
59
|
+
|
|
60
|
+
regularizer = self.prototypes.square().sum() / (2 * self.C)
|
|
61
|
+
return regularizer + margins.sum()
|
|
62
|
+
|
|
63
|
+
def step(self, x: torch.Tensor, y: torch.Tensor):
|
|
64
|
+
if self.backend == 'cpp':
|
|
65
|
+
with torch.no_grad():
|
|
66
|
+
mmhdc_cpp = get_mmhdc_cpp()
|
|
67
|
+
updated_prototypes = mmhdc_cpp.step(x, y, self.prototypes, self.lr, self.C)
|
|
68
|
+
self.prototypes.copy_(updated_prototypes)
|
|
69
|
+
elif self.backend == 'python':
|
|
70
|
+
self._py_step(x, y)
|
|
71
|
+
else:
|
|
72
|
+
raise ValueError(f"Unsupported backend '{self.backend}'. Expected 'cpp' or 'python'.")
|
|
73
|
+
|
|
74
|
+
@torch.no_grad()
|
|
75
|
+
def _py_step(self, x: torch.Tensor, y: torch.Tensor, optimized=True):
|
|
76
|
+
# This implementation is for reference purpose only. Use matrix-based optimized C++ and Python implementations
|
|
77
|
+
def _py_step_reference(x: torch.Tensor, y: torch.Tensor):
|
|
78
|
+
prototypes_update = torch.zeros_like(self.prototypes, dtype=self.dtype)
|
|
79
|
+
for cls in y.unique():
|
|
80
|
+
rolled_prototypes = torch.roll(self.prototypes, -cls.item(), dims=0)
|
|
81
|
+
x_cls = x[y == cls]
|
|
82
|
+
|
|
83
|
+
dot = x_cls @ (rolled_prototypes[0] - rolled_prototypes[1:]).T
|
|
84
|
+
hinge_loss = relu(2 - dot)
|
|
85
|
+
|
|
86
|
+
exceeding_margin = hinge_loss > 0
|
|
87
|
+
num_violations = exceeding_margin.sum(dim=1, dtype=x_cls.dtype)
|
|
88
|
+
prototypes_update[cls] += (x_cls * num_violations.unsqueeze(1)).sum(0)
|
|
89
|
+
|
|
90
|
+
y_true_all = exceeding_margin.any(0).nonzero().flatten()
|
|
91
|
+
for y_true in y_true_all:
|
|
92
|
+
prototypes_update[(y_true + 1 + cls) % self.num_classes] -= x_cls[exceeding_margin[:, y_true]].sum(0)
|
|
93
|
+
|
|
94
|
+
self.prototypes.data = (1 - self.lr / self.C) * self.prototypes.data + self.lr * prototypes_update
|
|
95
|
+
|
|
96
|
+
# Optimized code based on the C++ implementation logic
|
|
97
|
+
def _py_step_optimized(x: torch.Tensor, y: torch.Tensor):
|
|
98
|
+
scores = x @ self.prototypes.T
|
|
99
|
+
correct_scores = scores.gather(1, y.unsqueeze(1))
|
|
100
|
+
|
|
101
|
+
violated = (correct_scores - scores) < 2
|
|
102
|
+
violated.scatter_(1, y.unsqueeze(1), False)
|
|
103
|
+
|
|
104
|
+
W = -violated.to(x.dtype)
|
|
105
|
+
W.scatter_add_(1, y.unsqueeze(1), violated.sum(dim=1, keepdim=True).to(x.dtype))
|
|
106
|
+
|
|
107
|
+
prototypes_update = W.T @ x
|
|
108
|
+
self.prototypes.data = (1 - self.lr / self.C) * self.prototypes.data + self.lr * prototypes_update
|
|
109
|
+
|
|
110
|
+
_py_step_reference(x, y) if not optimized else _py_step_optimized(x, y)
|
mmhdc/utils/__init__.py
ADDED
mmhdc/utils/transform.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import math
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
class HDTransform(torch.nn.Module):
|
|
6
|
+
def __init__(
|
|
7
|
+
self,
|
|
8
|
+
in_channels: int,
|
|
9
|
+
out_channels: int,
|
|
10
|
+
seed: int = 0,
|
|
11
|
+
batch_size: Optional[int] = None,
|
|
12
|
+
normalize: bool = True,
|
|
13
|
+
device: str = 'cpu',
|
|
14
|
+
dtype: torch.dtype = torch.float32,):
|
|
15
|
+
|
|
16
|
+
super().__init__()
|
|
17
|
+
self.in_channels = in_channels
|
|
18
|
+
self.out_channels = out_channels
|
|
19
|
+
self.batch_size = batch_size
|
|
20
|
+
self.normalize = normalize
|
|
21
|
+
self.dtype = dtype
|
|
22
|
+
|
|
23
|
+
self._rng = torch.Generator(device="cpu")
|
|
24
|
+
self._rng.manual_seed(seed)
|
|
25
|
+
two_pi = torch.tensor(2.0 * math.pi, dtype=self.dtype)
|
|
26
|
+
self.register_buffer(
|
|
27
|
+
"_eps",
|
|
28
|
+
torch.tensor(1e-8, dtype=self.dtype, device=device),
|
|
29
|
+
persistent=False,
|
|
30
|
+
)
|
|
31
|
+
self.register_buffer(
|
|
32
|
+
"_G",
|
|
33
|
+
torch.randn(in_channels, out_channels, generator=self._rng, dtype=self.dtype).to(device),
|
|
34
|
+
persistent=False,
|
|
35
|
+
)
|
|
36
|
+
self.register_buffer(
|
|
37
|
+
"_phi",
|
|
38
|
+
torch.rand(out_channels, generator=self._rng, dtype=self.dtype).mul(two_pi).to(device),
|
|
39
|
+
persistent=False,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def _transform(self, x: torch.Tensor):
|
|
43
|
+
projection_matrix = self._G.unsqueeze(0).expand(x.size(0), -1, -1)
|
|
44
|
+
|
|
45
|
+
# OnlineHD feature mapping
|
|
46
|
+
buf1 = torch.bmm(x.unsqueeze(1), projection_matrix).squeeze(1)
|
|
47
|
+
buf2 = buf1 + self._phi
|
|
48
|
+
return buf1.sin_() * buf2.cos_()
|
|
49
|
+
|
|
50
|
+
def forward(self, data: torch.Tensor):
|
|
51
|
+
with torch.no_grad():
|
|
52
|
+
data = data.to(device=self._G.device, dtype=self.dtype)
|
|
53
|
+
if self.normalize:
|
|
54
|
+
norms = torch.norm(data, dim=1, keepdim=True)
|
|
55
|
+
data = data / torch.maximum(norms, self._eps)
|
|
56
|
+
|
|
57
|
+
if self.batch_size is None or self.batch_size >= data.size(0):
|
|
58
|
+
x_new = self._transform(data)
|
|
59
|
+
return x_new.cpu() if x_new.device.type != 'cpu' else x_new
|
|
60
|
+
|
|
61
|
+
x_new = torch.empty(data.size(0), self.out_channels, dtype=self.dtype)
|
|
62
|
+
for start in range(0, data.size(0), self.batch_size):
|
|
63
|
+
end = start + self.batch_size
|
|
64
|
+
batch = self._transform(data[start:end])
|
|
65
|
+
x_new[start:end].copy_(batch.cpu() if batch.device.type != 'cpu' else batch)
|
|
66
|
+
|
|
67
|
+
return x_new
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mmhdc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Author: Nikita Zeulin
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2026 Nikita Zeulin
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Requires-Python: >=3.11
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: torch
|
|
31
|
+
Provides-Extra: cpp
|
|
32
|
+
Requires-Dist: ninja; extra == "cpp"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# MM-HDC: Maximum-Margin Hyperdimensional Computing
|
|
36
|
+
|
|
37
|
+
This repository provides an implementation of the multi-class maximum-margin hyperdimensional computing (MM-HDC) classifier that adopts the optimization problem formulation of multi-class Weston-Watkins SVM to HDC.
|
|
38
|
+
|
|
39
|
+
The algorithm is implemented in the `MultiMMHDC` class, which can be imported as:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from mmhdc import MultiMMHDC
|
|
43
|
+
from mmhdc.utils import HDTransform
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
You can run the MNIST example from the GitHub repo as follows:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
git clone https://github.com/nzeulin/mmhdc.git && cd mmhdc
|
|
50
|
+
python -m pip install --upgrade pip && python -m pip install mmhdc
|
|
51
|
+
python -m pip install -r example/requirements.txt
|
|
52
|
+
python example/example.py --config example/mnist_config.py
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
C++ backend is highly recommended to use, as it can significantly accelerate the model training. In this case, use `python -m pip install mmhdc[cpp]`.
|
|
56
|
+
|
|
57
|
+
**NOTE:** If you don't have `gcc` installed in your system (required for compiling C++ module), you can install it as a Conda package into your venv: `conda install -c conda-forge gxx_linux-64`.
|
|
58
|
+
|
|
59
|
+
### Current features
|
|
60
|
+
- C++ backend (`libtorch`) to enable fast MM-HDC training.
|
|
61
|
+
- Support of floating-point prototypes and hypervectors.
|
|
62
|
+
|
|
63
|
+
### Citation
|
|
64
|
+
|
|
65
|
+
If you use this repository in your research, please cite it as software (paper in progress):
|
|
66
|
+
|
|
67
|
+
```bibtex
|
|
68
|
+
@software{zeulin_2026_mm_hdc,
|
|
69
|
+
author = {Zeulin, Nikita},
|
|
70
|
+
title = {MM-HDC: Maximum-Margin Hyperdimensional Computing},
|
|
71
|
+
year = {2026},
|
|
72
|
+
url = {https://github.com/nzeulin/mm-hdc}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
mmhdc/__init__.py,sha256=qqdxFdDJteLA5dCynf05KmwGNRNYatBepXfcxZvnggg,56
|
|
2
|
+
mmhdc/_cpp_backend.py,sha256=Op6fvnlRmGNYFbTSs62V6cCF0OLGH82MsybSFhYV3p4,730
|
|
3
|
+
mmhdc/model.py,sha256=mXev-BcVWySuKDJZ9PPusuhnzdqKgUCcPlon99hItLU,4755
|
|
4
|
+
mmhdc/cpp/mmhdc.cpp,sha256=q2ysINLkEbbAqxzNYYxNNrjxIKlv6OYWeLjNa5ls3ZM,2222
|
|
5
|
+
mmhdc/utils/__init__.py,sha256=OkPk7PiAuSSc8DvMJfCEon2Uzd1HuvWeFZsde6dcgqc,62
|
|
6
|
+
mmhdc/utils/transform.py,sha256=QgBqg4NGlCQgwGMIwVbOg4grlGiXwjMNiZrf8SlxPT0,2405
|
|
7
|
+
mmhdc-0.1.0.dist-info/licenses/LICENSE,sha256=gPQaAqyDO7WVlNm-korHqkViDn7Z8S1xDqcq74Rs-G8,1070
|
|
8
|
+
mmhdc-0.1.0.dist-info/METADATA,sha256=it4IUEDuxRH-WOAIDOqi95kgqrysl2tUBodCFa7FC5A,3041
|
|
9
|
+
mmhdc-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
10
|
+
mmhdc-0.1.0.dist-info/top_level.txt,sha256=IJcDmUlMuHSA7ub1Ge-093KZECZMQ4fFwF5DpL4X_sk,6
|
|
11
|
+
mmhdc-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nikita Zeulin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mmhdc
|