modelmark 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.
- modelmark/__init__.py +3 -0
- modelmark/__main__.py +5 -0
- modelmark/common/dataset.py +116 -0
- modelmark/common/downloader.py +88 -0
- modelmark/common/loader.py +63 -0
- modelmark/common/logger.py +34 -0
- modelmark/common/parser.py +134 -0
- modelmark/common/report.py +734 -0
- modelmark/common/tester.py +162 -0
- modelmark/common/utils.py +105 -0
- modelmark/config.py +97 -0
- modelmark/constants.py +34 -0
- modelmark/modelmark.py +206 -0
- modelmark/models/conv.py +75 -0
- modelmark/models/gru.py +45 -0
- modelmark/models/linear.py +65 -0
- modelmark/models/lstm.py +43 -0
- modelmark/models/transformer.py +86 -0
- modelmark-0.1.0.dist-info/METADATA +109 -0
- modelmark-0.1.0.dist-info/RECORD +24 -0
- modelmark-0.1.0.dist-info/WHEEL +5 -0
- modelmark-0.1.0.dist-info/entry_points.txt +2 -0
- modelmark-0.1.0.dist-info/licenses/LICENSE +21 -0
- modelmark-0.1.0.dist-info/top_level.txt +1 -0
modelmark/models/gru.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
|
|
4
|
+
from modelmark.common.utils import load_config
|
|
5
|
+
|
|
6
|
+
class GRUModel(nn.Module):
|
|
7
|
+
def __init__(self, input_dim : int, output_dim : int, context_size: int):
|
|
8
|
+
super().__init__()
|
|
9
|
+
|
|
10
|
+
# To avoid circular import, now it lives here
|
|
11
|
+
config = load_config()
|
|
12
|
+
|
|
13
|
+
self.input_size = input_dim
|
|
14
|
+
self.output_size = output_dim
|
|
15
|
+
self.context_size = context_size
|
|
16
|
+
|
|
17
|
+
self.hidden_size = config.model_config["GRU"]["hidden_size"]
|
|
18
|
+
self.num_layers = config.model_config["GRU"]["num_layers"]
|
|
19
|
+
|
|
20
|
+
self.gru = nn.GRU(
|
|
21
|
+
input_size = self.input_size,
|
|
22
|
+
hidden_size = self.hidden_size,
|
|
23
|
+
num_layers = self.num_layers,
|
|
24
|
+
batch_first = True,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
self.fc = nn.Linear(
|
|
28
|
+
self.hidden_size,
|
|
29
|
+
context_size * self.output_size,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def forward(self, x):
|
|
33
|
+
_, hn = self.gru(x)
|
|
34
|
+
|
|
35
|
+
# Last layer's hidden state
|
|
36
|
+
h = hn[-1]
|
|
37
|
+
|
|
38
|
+
out = self.fc(h)
|
|
39
|
+
out = out.view(
|
|
40
|
+
x.size(0),
|
|
41
|
+
self.context_size,
|
|
42
|
+
self.output_size,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return out
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Description:
|
|
3
|
+
|
|
4
|
+
This is an example of the Linear model definition.
|
|
5
|
+
You can write any other model, but the core components must remain intact:
|
|
6
|
+
|
|
7
|
+
1) The model __init__ core args must be included:
|
|
8
|
+
|
|
9
|
+
(input_dim : int, output_dim : int, context_size: int)
|
|
10
|
+
|
|
11
|
+
For mode parameter tweaking use your config file,
|
|
12
|
+
or define params right in the __init__() method (not recommended, config is more convenient).
|
|
13
|
+
|
|
14
|
+
2) The forward() method:
|
|
15
|
+
|
|
16
|
+
Input - [B, T, input_dim]
|
|
17
|
+
Output - [B, T, output_dim]
|
|
18
|
+
|
|
19
|
+
Currently only the continuous time-series data forecasting is available.
|
|
20
|
+
|
|
21
|
+
3) Differentiability:
|
|
22
|
+
|
|
23
|
+
The model should be differentiable, we will optimize it with Adam().
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import torch
|
|
28
|
+
import torch.nn as nn
|
|
29
|
+
|
|
30
|
+
# Switch to user's config when available
|
|
31
|
+
from modelmark.common.utils import load_config
|
|
32
|
+
|
|
33
|
+
class Linear(nn.Module):
|
|
34
|
+
def __init__(self, input_dim : int, output_dim : int, context_size: int):
|
|
35
|
+
super().__init__()
|
|
36
|
+
|
|
37
|
+
# To avoid circular import, now it lives here
|
|
38
|
+
config = load_config()
|
|
39
|
+
|
|
40
|
+
self.input_size = input_dim
|
|
41
|
+
self.output_size = output_dim
|
|
42
|
+
self.context_size = context_size
|
|
43
|
+
|
|
44
|
+
self.hidden_size = config.model_config["Linear"]["hidden_size"]
|
|
45
|
+
self.num_layers = config.model_config["Linear"]["num_layers"]
|
|
46
|
+
|
|
47
|
+
layers = [
|
|
48
|
+
nn.Linear(self.input_size, self.hidden_size),
|
|
49
|
+
nn.GELU()
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
for _ in range(self.num_layers - 1):
|
|
53
|
+
layers.append(nn.Linear(self.hidden_size, self.hidden_size))
|
|
54
|
+
layers.append(nn.GELU())
|
|
55
|
+
|
|
56
|
+
self.net = nn.Sequential(*layers)
|
|
57
|
+
|
|
58
|
+
self.fc = nn.Linear(self.hidden_size, self.output_size)
|
|
59
|
+
|
|
60
|
+
def forward(self, x):
|
|
61
|
+
|
|
62
|
+
x = self.net(x)
|
|
63
|
+
x = self.fc(x)
|
|
64
|
+
|
|
65
|
+
return x
|
modelmark/models/lstm.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
|
|
4
|
+
from modelmark.common.utils import load_config
|
|
5
|
+
|
|
6
|
+
class LSTMModel(nn.Module):
|
|
7
|
+
def __init__(self, input_dim: int, output_dim: int, context_size: int):
|
|
8
|
+
super().__init__()
|
|
9
|
+
|
|
10
|
+
# To avoid circular import, now it lives here
|
|
11
|
+
config = load_config()
|
|
12
|
+
|
|
13
|
+
self.input_size = input_dim
|
|
14
|
+
self.output_size = output_dim
|
|
15
|
+
self.context_size = context_size
|
|
16
|
+
self.hidden_size = config.model_config["LSTM"]["hidden_size"]
|
|
17
|
+
self.num_layers = config.model_config["LSTM"]["num_layers"]
|
|
18
|
+
|
|
19
|
+
# Change nn.GRU to nn.LSTM
|
|
20
|
+
self.lstm = nn.LSTM(
|
|
21
|
+
input_size=self.input_size,
|
|
22
|
+
hidden_size=self.hidden_size,
|
|
23
|
+
num_layers=self.num_layers,
|
|
24
|
+
batch_first=True,
|
|
25
|
+
)
|
|
26
|
+
self.fc = nn.Linear(
|
|
27
|
+
self.hidden_size,
|
|
28
|
+
context_size * self.output_size,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def forward(self, x):
|
|
32
|
+
|
|
33
|
+
_, (hn, cn) = self.lstm(x)
|
|
34
|
+
|
|
35
|
+
# Take the last layer's hidden state
|
|
36
|
+
h = hn[-1]
|
|
37
|
+
out = self.fc(h)
|
|
38
|
+
out = out.view(
|
|
39
|
+
x.size(0),
|
|
40
|
+
self.context_size,
|
|
41
|
+
self.output_size,
|
|
42
|
+
)
|
|
43
|
+
return out
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import torch.nn as nn
|
|
3
|
+
|
|
4
|
+
from modelmark.common.utils import load_config
|
|
5
|
+
|
|
6
|
+
class TransformerModel(nn.Module):
|
|
7
|
+
def __init__(self, input_dim : int, output_dim : int, context_size: int):
|
|
8
|
+
super().__init__()
|
|
9
|
+
|
|
10
|
+
# To avoid circular import, now it lives here
|
|
11
|
+
config = load_config()
|
|
12
|
+
|
|
13
|
+
self.input_size = input_dim
|
|
14
|
+
self.output_size = output_dim
|
|
15
|
+
self.context_size = context_size
|
|
16
|
+
self.max_seq_len = self.context_size
|
|
17
|
+
|
|
18
|
+
self.hidden_size = config.model_config["Transformer"]["hidden_size"]
|
|
19
|
+
self.num_layers = config.model_config["Transformer"]["num_layers"]
|
|
20
|
+
self.num_heads = config.model_config["Transformer"]["num_heads"]
|
|
21
|
+
self.dropout = config.model_config["Transformer"]["dropout"]
|
|
22
|
+
|
|
23
|
+
# Input projection
|
|
24
|
+
self.input_proj = nn.Linear(self.input_size, self.hidden_size,)
|
|
25
|
+
|
|
26
|
+
# Learnable positional embedding
|
|
27
|
+
self.pos_embedding = nn.Parameter(torch.zeros(1, self.max_seq_len, self.hidden_size))
|
|
28
|
+
|
|
29
|
+
# Transformer decoder layer
|
|
30
|
+
decoder_layer = nn.TransformerDecoderLayer(
|
|
31
|
+
d_model=self.hidden_size,
|
|
32
|
+
nhead=self.num_heads,
|
|
33
|
+
dim_feedforward=4 * self.hidden_size,
|
|
34
|
+
dropout=self.dropout,
|
|
35
|
+
batch_first=True,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
self.decoder = nn.TransformerDecoder(decoder_layer, num_layers=self.num_layers)
|
|
39
|
+
|
|
40
|
+
# Map hidden representation to outputs
|
|
41
|
+
self.fc = nn.Linear(self.hidden_size, self.output_size)
|
|
42
|
+
|
|
43
|
+
def forward(self, x):
|
|
44
|
+
# x:
|
|
45
|
+
# [batch, sequence_length, input_size]
|
|
46
|
+
|
|
47
|
+
batch_size, seq_len, _ = x.shape
|
|
48
|
+
|
|
49
|
+
if seq_len > self.pos_embedding.size(1):
|
|
50
|
+
raise ValueError(
|
|
51
|
+
f"Sequence length {seq_len} exceeds "
|
|
52
|
+
f"max_seq_len {self.pos_embedding.size(1)}"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Project input to transformer dimension
|
|
56
|
+
x = self.input_proj(x)
|
|
57
|
+
# [batch, sequence_length, hidden_size]
|
|
58
|
+
|
|
59
|
+
# Add positional information
|
|
60
|
+
x = x + self.pos_embedding[:, :seq_len, :]
|
|
61
|
+
|
|
62
|
+
# Causal mask:
|
|
63
|
+
# position i can only attend to positions <= i
|
|
64
|
+
mask = nn.Transformer.generate_square_subsequent_mask(
|
|
65
|
+
seq_len,
|
|
66
|
+
device=x.device,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Self-attention through decoder layers
|
|
70
|
+
x = self.decoder(
|
|
71
|
+
tgt=x,
|
|
72
|
+
memory=x,
|
|
73
|
+
tgt_mask=mask,
|
|
74
|
+
memory_mask=mask,
|
|
75
|
+
)
|
|
76
|
+
# [batch, sequence_length, hidden_size]
|
|
77
|
+
|
|
78
|
+
# Take the final context_size positions
|
|
79
|
+
x = x[:, -self.context_size:, :]
|
|
80
|
+
# [batch, context_size, hidden_size]
|
|
81
|
+
|
|
82
|
+
# Project to output
|
|
83
|
+
out = self.fc(x)
|
|
84
|
+
# [batch, context_size, output_size]
|
|
85
|
+
|
|
86
|
+
return out
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: modelmark
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Benchmark framework for neural network models
|
|
5
|
+
Author: Andrew Larin
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: numpy>=2.4
|
|
11
|
+
Requires-Dist: pandas>=3.0
|
|
12
|
+
Requires-Dist: torch>=2.12
|
|
13
|
+
Requires-Dist: tqdm>=4.67
|
|
14
|
+
Requires-Dist: playwright>=1.62
|
|
15
|
+
Requires-Dist: rich>=15.0
|
|
16
|
+
Requires-Dist: thop>=0.1
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# ModelMark
|
|
20
|
+
This tool will help you to test NN models against each other, and form a detailed report that is easy to embed to a website.
|
|
21
|
+
|
|
22
|
+
**Model evaluation report example:**
|
|
23
|
+
|
|
24
|
+
<img src="result.png" alt="Evaluation result" width="600">
|
|
25
|
+
|
|
26
|
+
## About:
|
|
27
|
+
|
|
28
|
+
At each testing run iteration, modelmark:
|
|
29
|
+
|
|
30
|
+
1) Selects next dataset, context, model and seed
|
|
31
|
+
2) Seeds the generators for reproducibility
|
|
32
|
+
3) Creates the loader, model and tester objects
|
|
33
|
+
4) Trains the model for E epochs, restores the state with the least validation loss
|
|
34
|
+
5) Tracks the GFLOPs, Memory, Time - all AVG over whole test
|
|
35
|
+
5) Evaluates the model on dataset with metrics from configuration file
|
|
36
|
+
6) Stores the mean result over S runs
|
|
37
|
+
|
|
38
|
+
That way, the more seeds you run, the more "fair" the results are.
|
|
39
|
+
Finally, modelmark will form the report with all the testing results, training stats and your machine metadata.
|
|
40
|
+
|
|
41
|
+
More on training stats:
|
|
42
|
+
|
|
43
|
+
Time - average time per epoch
|
|
44
|
+
Params - total number of model params
|
|
45
|
+
GFLOPs - average per batch
|
|
46
|
+
Peak Memory - max per training iteration
|
|
47
|
+
|
|
48
|
+
Test consists of F * C * M * S runs, where:
|
|
49
|
+
|
|
50
|
+
F - number of dataset files in the config (e.g. ["ETTh1" : ..., "Weather" : ...] - means F = 2)
|
|
51
|
+
C - number of context sizes (e.g. [32, 64, 128] - means C = 3)
|
|
52
|
+
M - number of models (e.g. ["Linear" : ..., "LSTM" : ...] - means M = 2)
|
|
53
|
+
S - number of seeds (e.g. [42, 43, 44] - means S = 2)
|
|
54
|
+
|
|
55
|
+
# Requirements:
|
|
56
|
+
|
|
57
|
+
OS: Windows or Linux
|
|
58
|
+
Python: 3.12+
|
|
59
|
+
|
|
60
|
+
# Usage:
|
|
61
|
+
|
|
62
|
+
1) Install the package
|
|
63
|
+
|
|
64
|
+
pip install "modelmark @ git+https://github.com/gloptim77/ModelMark.git"
|
|
65
|
+
|
|
66
|
+
2) Run the initialization in an empty folder
|
|
67
|
+
|
|
68
|
+
modelmark -t init
|
|
69
|
+
|
|
70
|
+
3.1) It will create two folders "modelmark_files" and "models"
|
|
71
|
+
|
|
72
|
+
In modelmark_files/config.py there are 3 main configs:
|
|
73
|
+
|
|
74
|
+
model_config = {...} - Models hyperparameters (number of layers, hidden dim, kernel size, etc.)
|
|
75
|
+
data_config = {...} - Dataset parameters (path to file, input/output features, train/val ratios, etc.)
|
|
76
|
+
test_config = {...} - Testing options (optimizer, loss criterion, metrics, learning rate, etc.)
|
|
77
|
+
|
|
78
|
+
3.2) (Optional) download the ETT dataset files
|
|
79
|
+
|
|
80
|
+
modelmark -t load
|
|
81
|
+
|
|
82
|
+
4) When your config, model and data are ready, you can run the testing
|
|
83
|
+
|
|
84
|
+
modelmark -t run
|
|
85
|
+
|
|
86
|
+
5) When the test is over, report results will be in files "result.html" and "result.png"
|
|
87
|
+
|
|
88
|
+
Example of the model: "src/modelmark/models/linear.py"
|
|
89
|
+
Example of the config: "src/modelmark/config.py"
|
|
90
|
+
|
|
91
|
+
You will find the detailed config example with description at [src/modelmark/config.py](src/modelmark/config.py).
|
|
92
|
+
|
|
93
|
+
Example Linear model with detailed description at [src/modelmark/models/linear.py](src/modelmark/models/linear.py).
|
|
94
|
+
|
|
95
|
+
You can change the config and add your model files to suit your test requirements.
|
|
96
|
+
|
|
97
|
+
(But make sure that config and models are compatible)
|
|
98
|
+
|
|
99
|
+
## Troubleshooting
|
|
100
|
+
|
|
101
|
+
- **If something doesn't work**
|
|
102
|
+
|
|
103
|
+
- Please open an issue and attach your application logs (found at "modelmark_files/modelmark.log") so I can help you troubleshoot.
|
|
104
|
+
- Try to restart
|
|
105
|
+
```python
|
|
106
|
+
modelmark -t restart
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- Try to manually delete "modelmark_files" and return to Usage->2)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
modelmark/__init__.py,sha256=cJHYdio7T5sb5Y65QxncymbNMU4WfOOUqvPvtpDuIN8,56
|
|
2
|
+
modelmark/__main__.py,sha256=_b2ICXLS4APDLGxjTRNhc17k36SGlpACeOsFW7nLGkk,81
|
|
3
|
+
modelmark/config.py,sha256=OF4aS4QMx70fd57f31iteEVHo6N2TMSCyRBZPcv36u4,2508
|
|
4
|
+
modelmark/constants.py,sha256=lSlR-TmxGhSFih3zJMZ6vCtYTUmVwFqFj1D-aM52K1Y,1096
|
|
5
|
+
modelmark/modelmark.py,sha256=le6cVW2xGiCpWzBU6UmnuOOrlebh-oQn9MWGA2lu500,7714
|
|
6
|
+
modelmark/common/dataset.py,sha256=Ahyux0rcWxgI3YHkPS-ApHlyBKZL5p81eXz1YkiV--o,2663
|
|
7
|
+
modelmark/common/downloader.py,sha256=va2cqga4Tii6uqJgP79RRLhtDH3I2cNSlvjtBox-NiM,2666
|
|
8
|
+
modelmark/common/loader.py,sha256=SYv8FDscfZbVQ4IaTxcwfsuFBL4t3JNxL3y_3KS-rEE,1769
|
|
9
|
+
modelmark/common/logger.py,sha256=1cNMADpRxtVWMLdncJIfmrEzrBi4S17MLomeEeiFeH8,732
|
|
10
|
+
modelmark/common/parser.py,sha256=T1OkpjAs1VYwmElZOPe1k4j5wvEdnP-YnCsN4v9NY10,3429
|
|
11
|
+
modelmark/common/report.py,sha256=soOxwfk3VFjLwf730-1jsCMtbvxCDOsNIp7uEa1B7Co,19778
|
|
12
|
+
modelmark/common/tester.py,sha256=mDPW7oIN9MnwchuBKwYTvMfAAOwmc7d6BALbCqe0jq8,4335
|
|
13
|
+
modelmark/common/utils.py,sha256=O3HDrC47KnJVe8SsjlX1ynj499VDue613Jrcko4Nzbw,2526
|
|
14
|
+
modelmark/models/conv.py,sha256=MoeaXcXFYjibR5SXVgMWJiXVWpl0v5x2p8SE01Uo5F4,1678
|
|
15
|
+
modelmark/models/gru.py,sha256=tEo2goO1INQ8BObXsbSk3M8e9ZqwZJn70gCaA-oDiAM,943
|
|
16
|
+
modelmark/models/linear.py,sha256=GHRpzhz-zJCQd6A2EimDoRzOrYLm8qoGPIXfUcxQVsg,1616
|
|
17
|
+
modelmark/models/lstm.py,sha256=4FVOyxxXjNpde9d9YdqN_nE1d3DA2RRLE2G4uHwiM5o,982
|
|
18
|
+
modelmark/models/transformer.py,sha256=EjVQgO7-AeHLK1mADscuN7YAaYvSvzGQiIx524wNIHQ,2315
|
|
19
|
+
modelmark-0.1.0.dist-info/licenses/LICENSE,sha256=HtjFfP8fZ8LTdjFbSqhQKjt_4uZophTprjczqbTHxLo,1069
|
|
20
|
+
modelmark-0.1.0.dist-info/METADATA,sha256=rnnz-MDRrfR4yNx3bsb65V9Kntrnc4HPGAYLkEvLMD0,3560
|
|
21
|
+
modelmark-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
22
|
+
modelmark-0.1.0.dist-info/entry_points.txt,sha256=EpoYh6jh3uO9kKoDVkJJtsAtpF0EJYO8YRBUemwnzaE,54
|
|
23
|
+
modelmark-0.1.0.dist-info/top_level.txt,sha256=l54tAXR7NL0-JVAci_RRPJOBtFNlRduLps5WR3Qg6g0,10
|
|
24
|
+
modelmark-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrew Larin
|
|
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
|
+
modelmark
|