patra-toolkit 0.1.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.
@@ -0,0 +1,3 @@
1
+ from .patra_model_card import *
2
+ from .fairlearn_bias import *
3
+ from .shap_xai import *
@@ -0,0 +1,32 @@
1
+ from fairlearn.metrics import demographic_parity_difference, equalized_odds_difference
2
+
3
+
4
+ class BiasAnalyzer:
5
+ """
6
+ Provides automated capture of fairness metrics in a given AI workflow.
7
+ """
8
+ def __init__(self, dataset, true_labels, predicted_labels, sensitive_feature_name, sensitive_feature_data, model):
9
+ self.dataset = dataset
10
+ self.true_labels = true_labels
11
+ self.predicted_labels = predicted_labels
12
+ self.sensitive_feature = sensitive_feature_name
13
+ self.sensitive_data = sensitive_feature_data
14
+ self.model = model
15
+
16
+ def calculate_bias_metrics(self):
17
+ """
18
+ Calculate bias metrics.
19
+
20
+ Args:
21
+ None
22
+
23
+ Returns:
24
+ dict: A dictionary of bias
25
+ """
26
+ demographic_parity = demographic_parity_difference(self.true_labels, self.predicted_labels,
27
+ sensitive_features=self.sensitive_data)
28
+ equal_odds_diff = equalized_odds_difference(self.true_labels, self.predicted_labels,
29
+ sensitive_features=self.sensitive_data)
30
+
31
+ return {"demographic_parity_diff": demographic_parity,
32
+ "equal_odds_difference": equal_odds_diff}
@@ -0,0 +1,376 @@
1
+ import hashlib
2
+ import json
3
+ import os.path
4
+ from dataclasses import dataclass, field
5
+ from json import JSONEncoder
6
+ from typing import List, Optional, Dict
7
+
8
+ import jsonschema
9
+ import pkg_resources
10
+ import requests
11
+
12
+ from .fairlearn_bias import BiasAnalyzer
13
+ from .shap_xai import ExplainabilityAnalyser
14
+
15
+ SCHEMA_JSON = os.path.join(os.path.dirname(__file__), 'schema', 'schema.json')
16
+
17
+
18
+ @dataclass
19
+ class Metric:
20
+ """
21
+ Data class for storing metric key-value pairs.
22
+
23
+ Args:
24
+ key (str): The name of the metric.
25
+ value (str): The value of the metric.
26
+ """
27
+ key: str
28
+ value: str
29
+
30
+ @dataclass
31
+ class AIModel:
32
+ """
33
+ Represents and stores AI model metadata and its performance metrics.
34
+
35
+ Args:
36
+ name (str): The name of the model.
37
+ version (str): The version identifier of the model.
38
+ description (str): A detailed description of the model.
39
+ owner (str): The owner of the model.
40
+ location (str): The file path or URL where the model is stored.
41
+ license (str): The license under which the model is distributed.
42
+ framework (str): The framework used to build the model (e.g., TensorFlow, PyTorch).
43
+ model_type (str): The type of model (e.g., classifier, regressor).
44
+ test_accuracy (str): The accuracy of the model on a test dataset.
45
+ model_structure (str): The structure of the model as a dictionary (optional).
46
+ metrics (str): A dictionary storing performance metrics for the model.
47
+
48
+ Example:
49
+ .. code-block:: python
50
+
51
+ ai_model = AIModel(
52
+ name="Model Name",
53
+ version="1.0",
54
+ description="Model description",
55
+ owner="Model owner",
56
+ location="Model location",
57
+ license="Model license",
58
+ framework="Model framework",
59
+ model_type="Model type",
60
+ test_accuracy=0.95,
61
+ model_structure={},
62
+ metrics={"accuracy": "0.95"}
63
+ )
64
+ """
65
+ name: str
66
+ version: str
67
+ description: str
68
+ owner: str
69
+ location: str
70
+ license: str
71
+ framework: str
72
+ model_type: str
73
+ test_accuracy: float
74
+ model_structure: Optional[object] = field(default_factory=dict)
75
+ metrics: Dict[str, str] = field(default_factory=dict)
76
+
77
+ def add_metric(self, key: str, value: str) -> None:
78
+ """
79
+ Adds a performance metric to the model's metrics.
80
+
81
+ Args:
82
+ key (str): The name of the metric.
83
+ value (str): The value of the metric.
84
+
85
+ Returns:
86
+ None
87
+ """
88
+ self.metrics[key] = value
89
+
90
+ def remove_nulls(self, model_structure):
91
+ """
92
+ Recursively removes null values from the model structure.
93
+
94
+ Args:
95
+ model_structure (object): The model structure as a dictionary or list.
96
+
97
+ Returns:
98
+ object: Model structure with null values removed.
99
+ """
100
+ if isinstance(model_structure, dict):
101
+ return {k: self.remove_nulls(v) for k, v in model_structure.items() if v is not None}
102
+ elif isinstance(model_structure, list):
103
+ return [self.remove_nulls(v) for v in model_structure if v is not None]
104
+ return model_structure
105
+
106
+ def populate_model_structure(self, trained_model):
107
+ """
108
+ Populates the `model_structure` attribute from a trained model object.
109
+
110
+ Args:
111
+ trained_model (object): A trained machine learning model object.
112
+
113
+ Returns:
114
+ None
115
+ """
116
+ if self.framework == 'tensorflow':
117
+ json_structure = json.loads(trained_model.to_json())
118
+ self.model_structure = self.remove_nulls(json_structure)
119
+ else:
120
+ self.model_structure = {}
121
+
122
+
123
+ @dataclass
124
+ class BiasAnalysis:
125
+ """
126
+ Class to store results from bias analysis.
127
+
128
+ Args:
129
+ demographic_parity_difference (float): The difference in demographic parity between groups.
130
+ equal_odds_difference (float): The difference in equal odds between groups.
131
+ """
132
+ demographic_parity_difference: float
133
+ equal_odds_difference: float
134
+
135
+
136
+ @dataclass
137
+ class ExplainabilityAnalysis:
138
+ """
139
+ Class to store explainability metrics.
140
+
141
+ Args:
142
+ name (str): Name of the explainability method used.
143
+ metrics (List[Metric]): List of metrics related to explainability analysis.
144
+ """
145
+ name: str
146
+ metrics: List[Metric] = field(default_factory=list)
147
+
148
+
149
+ @dataclass
150
+ class ModelCard:
151
+ """
152
+ Represents an AI model card to document model metadata, analyses, and requirements.
153
+
154
+ Args:
155
+ name (str): The name of the model.
156
+ version (str): The model's version.
157
+ short_description (str): A brief description of the model.
158
+ full_description (str): A detailed description of the model.
159
+ keywords (str): Comma-separated keywords for searchability.
160
+ author (str): The model's creator or owner.
161
+ input_type (str): Type of input data (e.g., "Image", "Text").
162
+ category (str): The category of the model (e.g., "Classification", "Regression").
163
+ input_data (Optional[str]): Description of the model's input data.
164
+ output_data (Optional[str]): Description of the model's output data.
165
+ foundational_model (Optional[str]): Reference to any foundational model used.
166
+ ai_model (Optional[AIModel]): An instance of `AIModel` containing model details.
167
+ bias_analysis (Optional[BiasAnalysis]): Instance of `BiasAnalysis` containing bias metrics.
168
+ xai_analysis (Optional[ExplainabilityAnalysis]): Instance of `ExplainabilityAnalysis` with interpretability metrics.
169
+ model_requirements (Optional[List[str]]): List of required packages and dependencies.
170
+ id (Optional[str]): Unique identifier for the model card, generated upon submission.
171
+
172
+ Example:
173
+ .. code-block:: python
174
+
175
+ model_card = ModelCard(
176
+ name="Model Name",
177
+ version="1.0",
178
+ short_description="A brief description",
179
+ full_description="A detailed description of the model's purpose and usage.",
180
+ keywords="classification, AI, image processing",
181
+ author="Author Name",
182
+ input_type="Image",
183
+ category="Classification",
184
+ input_data="Images of size 28x28.",
185
+ output_data="Prediction probabilities for classes.",
186
+ foundational_model="Base Model Reference",
187
+ ai_model=AIModel(
188
+ name="Model Name",
189
+ version="1.0",
190
+ description="Detailed model description",
191
+ owner="Model owner",
192
+ location="Storage location",
193
+ license="MIT",
194
+ framework="TensorFlow",
195
+ model_type="Classifier",
196
+ test_accuracy=0.95,
197
+ model_structure={},
198
+ metrics={"accuracy": "0.95"}
199
+ ),
200
+ bias_analysis=BiasAnalysis(
201
+ demographic_parity_difference=0.05,
202
+ equal_odds_difference=0.1
203
+ ),
204
+ xai_analysis=ExplainabilityAnalysis(
205
+ name="SHAP",
206
+ metrics=[Metric(key="Feature A", value="0.1")]
207
+ ),
208
+ model_requirements=["numpy>=1.19.2", "tensorflow>=2.4.1"]
209
+ )
210
+ """
211
+ name: str
212
+ version: str
213
+ short_description: str
214
+ full_description: str
215
+ keywords: str
216
+ author: str
217
+ input_type: str
218
+ category: str
219
+ input_data: Optional[str] = ""
220
+ output_data: Optional[str] = ""
221
+ foundational_model: Optional[str] = ""
222
+ ai_model: Optional[AIModel] = None
223
+ bias_analysis: Optional[BiasAnalysis] = None
224
+ xai_analysis: Optional[ExplainabilityAnalysis] = None
225
+ model_requirements: Optional[List] = None
226
+ id: Optional[str] = field(init=False, default=None)
227
+
228
+ def __str__(self):
229
+ """
230
+ Returns a JSON string representation of the model card.
231
+
232
+ Returns:
233
+ str: A JSON-formatted string representing the model card.
234
+ """
235
+ return json.dumps(self.__dict__, cls=ModelCardJSONEncoder, indent=4, separators=(',', ': '))
236
+
237
+ def populate_bias(self, dataset, true_labels, predicted_labels, sensitive_feature_name, sensitive_feature_data, model):
238
+ """
239
+ Calculates and stores fairness metrics.
240
+
241
+ Args:
242
+ dataset (object): The dataset used for bias analysis.
243
+ true_labels (list): The ground truth labels.
244
+ predicted_labels (list): Model's predictions.
245
+ sensitive_feature_name (str): The name of the sensitive attribute.
246
+ sensitive_feature_data (list): Values for the sensitive feature.
247
+ model (object): The model being analyzed.
248
+
249
+ Returns:
250
+ None
251
+ """
252
+ bias_analyzer = BiasAnalyzer(dataset, true_labels, predicted_labels, sensitive_feature_name,
253
+ sensitive_feature_data, model)
254
+ self.bias_analysis = bias_analyzer.calculate_bias_metrics()
255
+
256
+ def populate_xai(self, train_dataset, column_names, model, n_features=10):
257
+ """
258
+ Computes and stores feature importance metrics.
259
+
260
+ Args:
261
+ train_dataset (object): Training dataset used in the analysis.
262
+ column_names (list): Names of the features.
263
+ model (object): The model being explained.
264
+ n_features (int, optional): Number of features to analyze. Default is 10.
265
+
266
+ Returns:
267
+ None
268
+ """
269
+ xai_analyzer = ExplainabilityAnalyser(train_dataset, column_names, model)
270
+ self.xai_analysis = xai_analyzer.calculate_xai_features(n_features)
271
+
272
+ def populate_requirements(self):
273
+ """
274
+ Collects package requirements for the model card, excluding specific dependencies.
275
+
276
+ Returns:
277
+ None
278
+ """
279
+ exclude_packages = {"shap", "fairlearn"}
280
+ installed_packages = pkg_resources.working_set
281
+ packages_list = sorted(["%s==%s" % (i.key, i.version) for i in installed_packages])
282
+ self.model_requirements = [pkg for pkg in packages_list if pkg.split("==")[0] not in exclude_packages]
283
+
284
+ def validate(self):
285
+ """
286
+ Validates the model card against a predefined JSON schema.
287
+
288
+ Returns:
289
+ bool: True if the model card is valid according to the schema, False otherwise.
290
+ """
291
+ mc_json = self.__str__()
292
+ try:
293
+ with open(SCHEMA_JSON, 'r') as schema_file:
294
+ schema = json.load(schema_file)
295
+ jsonschema.validate(instance=json.loads(mc_json), schema=schema)
296
+ return True
297
+ except jsonschema.ValidationError as e:
298
+ print(e.message)
299
+ return False
300
+ except Exception as e:
301
+ print(f"An unexpected error occurred: {e}")
302
+ return False
303
+
304
+ def submit(self, patra_server_url):
305
+ """
306
+ Validates and submits the model card to the specified Patra server.
307
+
308
+ Args:
309
+ patra_server_url (str): The Patra server URL where the model card should be submitted.
310
+
311
+ Returns:
312
+ dict: The server's response as a JSON object.
313
+ """
314
+ if self.validate():
315
+ try:
316
+ self.id = self._get_hash_id(patra_server_url)
317
+ patra_submit_url = f"{patra_server_url}/upload_mc"
318
+ headers = {'Content-Type': 'application/json'}
319
+ response = requests.post(patra_submit_url, json=json.loads(str(self)), headers=headers)
320
+ response.raise_for_status()
321
+ return response.json()
322
+ except requests.exceptions.RequestException as e:
323
+ print("The Patra Server cannot be reached. Please try again.")
324
+ return None
325
+ return {"An error occurred: valid patra_server_url not provided. Unable to upload."}
326
+
327
+ def _get_hash_id(self, patra_server_url):
328
+ """
329
+ Generates a unique identifier for the model card based on its metadata.
330
+
331
+ Args:
332
+ patra_server_url (str): The Patra server URL used to generate the ID.
333
+
334
+ Returns:
335
+ str: A unique hash identifier for the model card.
336
+ """
337
+ combined_string = f"{self.name}:{self.version}:{self.author}"
338
+ try:
339
+ if patra_server_url:
340
+ patra_hash_url = f"{patra_server_url}/get_hash_id"
341
+ headers = {'Content-Type': 'application/json'}
342
+ response = requests.get(patra_hash_url, params={"combined_string": combined_string}, headers=headers)
343
+ response.raise_for_status()
344
+ return response.json()
345
+ else:
346
+ return hashlib.sha256(combined_string.encode()).hexdigest()
347
+ except requests.exceptions.RequestException as e:
348
+ print("Could not connect to the Patra Server, generating the ID locally")
349
+ return hashlib.sha256(combined_string.encode()).hexdigest()
350
+
351
+ def save(self, file_location):
352
+ """
353
+ Saves the model card as a JSON file to the specified location.
354
+
355
+ Args:
356
+ file_location (str): The path where the model card JSON file will be saved.
357
+
358
+ Returns:
359
+ None
360
+ """
361
+ with open(file_location, 'w') as json_file:
362
+ json_file.write(str(self))
363
+
364
+
365
+ class ModelCardJSONEncoder(JSONEncoder):
366
+ """
367
+ Custom JSON Encoder for ModelCard to handle complex objects.
368
+
369
+ Methods:
370
+ default: Serializes non-serializable fields.
371
+ """
372
+
373
+ def default(self, obj):
374
+ if isinstance(obj, (ModelCard, Metric, AIModel, ExplainabilityAnalysis, BiasAnalysis)):
375
+ return obj.__dict__
376
+ return super().default(obj)
@@ -0,0 +1,274 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "Patra Model Card",
4
+ "type": "object",
5
+ "commonFields": {
6
+ "urlOrDoi": {
7
+ "oneOf": [
8
+ {
9
+ "pattern": "^(https?|ftp):\\/\\/[^\\s/$.?#].[\\S]*$",
10
+ "description": "Must be a valid URL"
11
+ },
12
+ {
13
+ "pattern": "^10\\.\\d{4,9}/[^\\s]+$",
14
+ "description": "or valid DOI"
15
+ },
16
+ {
17
+ "enum": [""],
18
+ "description": "or empty"
19
+ },
20
+ {
21
+ "type": "null"
22
+ }
23
+ ]
24
+ }
25
+ },
26
+ "properties": {
27
+ "id": {
28
+ "type": "string",
29
+ "description": "The id of the Model Card"
30
+ },
31
+ "name": {
32
+ "type": "string",
33
+ "description": "The name of the Model Card"
34
+ },
35
+ "version": {
36
+ "type": "string",
37
+ "description": "The version of the Model Card"
38
+ },
39
+ "short_description": {
40
+ "type": "string",
41
+ "description": "A brief description of the Model Card"
42
+ },
43
+ "full_description": {
44
+ "type": "string",
45
+ "description": "A comprehensive description of the Model Card"
46
+ },
47
+ "keywords": {
48
+ "type": "string",
49
+ "description": "Keywords for the model card"
50
+ },
51
+ "author": {
52
+ "type": "string",
53
+ "description": "The author or creator of the Model Card"
54
+ },
55
+ "input_data": {
56
+ "$ref": "#/commonFields/urlOrDoi"
57
+ },
58
+ "input_type": {
59
+ "type": "string",
60
+ "description": "Type of the input data"
61
+ },
62
+ "output_data": {
63
+ "$ref": "#/commonFields/urlOrDoi"
64
+ },
65
+ "foundational_model": {
66
+ "description": "If a foundational model has been used, provide the model ID",
67
+ "type": "string"
68
+ },
69
+ "category": {
70
+ "type": "string",
71
+ "enum": [
72
+ "classification",
73
+ "regression",
74
+ "clustering",
75
+ "anomaly detection",
76
+ "dimensionality reduction",
77
+ "reinforcement learning",
78
+ "natural language processing",
79
+ "computer vision",
80
+ "recommendation systems",
81
+ "time series forecasting",
82
+ "graph learning",
83
+ "graph neural networks",
84
+ "generative modeling",
85
+ "transfer learning",
86
+ "self-supervised learning",
87
+ "semi-supervised learning",
88
+ "unsupervised learning",
89
+ "causal inference",
90
+ "multi-task learning",
91
+ "metric learning",
92
+ "density estimation",
93
+ "multi-label classification",
94
+ "ranking",
95
+ "structured prediction",
96
+ "neural architecture search",
97
+ "sequence modeling",
98
+ "embedding learning",
99
+ "other"
100
+ ]
101
+ },
102
+ "documentation": {
103
+ "type": "string",
104
+ "description": "URL for documentation if available"
105
+ },
106
+ "ai_model": {
107
+ "description": "Description of the AI Model",
108
+ "type": [
109
+ "object"
110
+ ],
111
+ "properties": {
112
+ "name": {
113
+ "description": "The name of the model.",
114
+ "type": "string"
115
+ },
116
+ "version": {
117
+ "type": "string",
118
+ "description": "The version of the Model"
119
+ },
120
+ "description": {
121
+ "description": "Description of the AI Model.",
122
+ "type": "string"
123
+ },
124
+ "owner": {
125
+ "type": "string",
126
+ "description": "The owner of the Model"
127
+ },
128
+ "location": {
129
+ "description": "Downloadable URL of the model",
130
+ "type": "string"
131
+ },
132
+ "license": {
133
+ "description": "Licence the model",
134
+ "type": "string"
135
+ },
136
+ "model_structure": {
137
+ "description": "Structure of the model",
138
+ "type": "object"
139
+ },
140
+ "framework": {
141
+ "type": "string",
142
+ "enum": [
143
+ "sklearn",
144
+ "tensorflow",
145
+ "pytorch",
146
+ "other"
147
+ ]
148
+ },
149
+ "model_type": {
150
+ "type": "string",
151
+ "enum": [
152
+ "cnn",
153
+ "decision_tree",
154
+ "dnn",
155
+ "rnn",
156
+ "svm",
157
+ "kmeans",
158
+ "llm",
159
+ "random_forest",
160
+ "lstm",
161
+ "gnn",
162
+ "other"
163
+ ]
164
+ },
165
+ "test_accuracy": {
166
+ "description": "Accuracy of the model for the test data",
167
+ "type": "number"
168
+ },
169
+ "model_metrics": {
170
+ "description": "Metrics of the AI Model",
171
+ "type": "array",
172
+ "items": {
173
+ "$ref": "#/fields/metrics"
174
+ }
175
+ }
176
+ },
177
+ "required": [
178
+ "name",
179
+ "version",
180
+ "description",
181
+ "owner",
182
+ "location",
183
+ "license",
184
+ "framework",
185
+ "test_accuracy",
186
+ "model_type"
187
+ ]
188
+ },
189
+ "bias_analysis": {
190
+ "type": ["object", "null"],
191
+ "properties": {
192
+ "demographic_parity_diff": {
193
+ "type": "number"
194
+ },
195
+ "equal_odds_difference": {
196
+ "type": "number"
197
+ }
198
+ }
199
+ },
200
+ "xai_analysis": {
201
+ "description": "Explainability analysis of the Model Card.",
202
+ "type": [
203
+ "object",
204
+ "null"
205
+ ],
206
+ "properties": {
207
+ "bias_metrics": {
208
+ "description": "Metrics of the bias scanner",
209
+ "type": "array",
210
+ "items": {
211
+ "$ref": "#/fields/metrics"
212
+ }
213
+ }
214
+ }
215
+ },
216
+ "model_requirements": {
217
+ "type": [
218
+ "array",
219
+ "null"
220
+ ],
221
+ "items": {
222
+ "type": "string"
223
+ }
224
+ }
225
+ },
226
+ "required": [
227
+ "name",
228
+ "version",
229
+ "short_description",
230
+ "full_description",
231
+ "author",
232
+ "keywords",
233
+ "input_data",
234
+ "input_type",
235
+ "output_data",
236
+ "ai_model"
237
+ ],
238
+ "fields": {
239
+ "metrics": {
240
+ "type": "object",
241
+ "properties": {
242
+ "key": {
243
+ "description": "Name of the metric",
244
+ "type": "string"
245
+ },
246
+ "value": {
247
+ "description": "The value of the performance metric.",
248
+ "type": "string"
249
+ }
250
+ }
251
+ }
252
+ },
253
+ "commonFields": {
254
+ "urlOrDoi": {
255
+ "oneOf": [
256
+ {
257
+ "pattern": "^(https?|ftp):\\/\\/[^\\s/$.?#].[\\S]*$",
258
+ "description": "Must be a valid URL"
259
+ },
260
+ {
261
+ "pattern": "^10\\.\\d{4,9}/[^\\s]+$",
262
+ "description": "or valid DOI"
263
+ },
264
+ {
265
+ "enum": [""],
266
+ "description": "or empty"
267
+ },
268
+ {
269
+ "type": "null"
270
+ }
271
+ ]
272
+ }
273
+ }
274
+ }
@@ -0,0 +1,37 @@
1
+ import shap
2
+ import pandas as pd
3
+ import re
4
+
5
+ class ExplainabilityAnalyser:
6
+ """
7
+ Provides automated capture of xai information in a given AI workflow.
8
+ """
9
+ def __init__(self, train_dataset, column_names, model):
10
+ self.dataset = train_dataset
11
+ self.column_names = column_names
12
+ self.model = model
13
+
14
+ def calculate_xai_features(self, n_features=10, pytorch = False):
15
+ # calculate the shap values
16
+ if pytorch:
17
+ explainer = shap.DeepExplainer(self.model, self.dataset)
18
+ else:
19
+ explainer = shap.Explainer(self.model, self.dataset)
20
+
21
+ values = explainer(self.dataset)
22
+ shap_values = values.values
23
+
24
+ if len(shap_values.shape) == 3:
25
+ shap_values = abs(shap_values).mean(axis=2)
26
+
27
+ feature_importance_df = pd.DataFrame(shap_values, columns=self.column_names).abs().mean(axis=0)
28
+ top_features = feature_importance_df.sort_values(ascending=False).head(n_features)
29
+
30
+ result_dict = {}
31
+
32
+ for name, importance in top_features.items():
33
+
34
+ filtered_name = re.sub(r'[^a-zA-Z0-9]', '_', name)
35
+ result_dict[filtered_name] = float(importance)
36
+
37
+ return result_dict
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2024, Indiana University Board of Trustees.
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.1
2
+ Name: patra-toolkit
3
+ Version: 0.1.1
4
+ Summary: Toolkit for semi-automated modelcard creation for AI/ML models.
5
+ Home-page: https://github.com/Data-to-Insight-Center/patra-toolkit.git
6
+ Author: Data to Insight Center
7
+ Author-email: d2i@iu.edu
8
+ License: BSD-3-Clause
9
+ License-File: LICENSE
10
+ Requires-Dist: jsonschema>4.18.5
11
+ Requires-Dist: fairlearn~=0.11.0
12
+ Requires-Dist: shap~=0.46.0
13
+ Requires-Dist: pandas>=2.0.0
14
+ Requires-Dist: numpy>2.0.0
15
+ Requires-Dist: requests>2.32.2
16
+
@@ -0,0 +1,14 @@
1
+ patra_toolkit/__init__.py,sha256=Ivigf1Cp-Lweizl81EkDOIoo-otYDsfCZbNjY3IjoJc,86
2
+ patra_toolkit/fairlearn_bias.py,sha256=Zd-bItKO3GpX-5rQWEM3GApLFUitGWzXkl5y8fvu2mY,1264
3
+ patra_toolkit/patra_model_card.py,sha256=OeELQrcT4cX47hblkozrYUwqQnCmyN0ng0C5IiQa7Yc,13828
4
+ patra_toolkit/shap_xai.py,sha256=QnEx39rtO-V_QS3FyL4gHmejyG_S-mf14IpCNWn3Lzs,1206
5
+ patra_toolkit/schema/schema.json,sha256=dW9GkAGWT4FVXWPBBlGkHB40P6O4RGntXxuiPG4Udd4,6384
6
+ tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ tests/test_patra_hash_id.py,sha256=AhQROssw1uFWjEG-asBsS4hQpAH7uflYMAzxw-U89S8,4380
8
+ tests/test_patra_inout_data.py,sha256=36EqCE0f1KWJsRs2oxlLQ_9Ubtp8k6eAtOtA3rjn54o,3628
9
+ tests/test_patra_mc.py,sha256=ucUU5zLu-Fl1PpohDF0j9uSyMkSVkVNoNojrrldp0nY,5553
10
+ patra_toolkit-0.1.1.dist-info/LICENSE,sha256=mvKzUSZOUwnUYJ-kO0fY0JHMNzkSSXML5n4WED8plpc,1524
11
+ patra_toolkit-0.1.1.dist-info/METADATA,sha256=nU5ZSsmNuFtRURCfTbaj1RQ8s_UIbId5KB0HazVhUNE,483
12
+ patra_toolkit-0.1.1.dist-info/WHEEL,sha256=a7TGlA-5DaHMRrarXjVbQagU3Man_dCnGIWMJr5kRWo,91
13
+ patra_toolkit-0.1.1.dist-info/top_level.txt,sha256=bAHB9O6o1rDnP-Cbo3EOXY7wRsi6HS5X5wi9jMuCRaQ,20
14
+ patra_toolkit-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.4.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ patra_toolkit
2
+ tests
tests/__init__.py ADDED
File without changes
@@ -0,0 +1,116 @@
1
+ import hashlib
2
+ import os
3
+
4
+ import unittest
5
+ from unittest.mock import patch, MagicMock
6
+ import requests
7
+
8
+ from patra_toolkit import ModelCard, AIModel, BiasAnalysis, ExplainabilityAnalysis, Metric
9
+
10
+ SCHEMA_JSON = os.path.join(os.path.dirname(__file__), os.pardir,
11
+ 'patra_toolkit/schema/schema.json')
12
+
13
+
14
+ class ModelCardTestCase2(unittest.TestCase):
15
+
16
+ def setUp(self) -> None:
17
+ self.aimodel = AIModel(
18
+ name="DenseNet",
19
+ version="v0.1",
20
+ description="DenseNet CNN model",
21
+ owner="PyTorch",
22
+ location="pytorch.org",
23
+ license="testLicence",
24
+ framework="tensorflow",
25
+ model_type="cnn",
26
+ test_accuracy=0.89
27
+ )
28
+ self.bias_analysis = BiasAnalysis(0.1, 0.2)
29
+ self.xai_analysis = ExplainabilityAnalysis("XAI Test 2", [Metric("xai1", 0.5), Metric("xai2", 0.8)])
30
+ self.mc_save_location = "./test_mc.json"
31
+
32
+ @patch('requests.get')
33
+ def test_get_hash_id_success(self, mock_get):
34
+ """Test that id is set correctly when server responds successfully."""
35
+ # Mock the server response to return a hash ID
36
+ mock_get.return_value = MagicMock(status_code=200)
37
+ mock_get.return_value.json.return_value = "e6c22bdf9fd3c164a2a9a083fb56fca9328f6ca30f7dcd2ebfc140a7d6f02149"
38
+
39
+ model_card = ModelCard(
40
+ name="icicle-camera-traps",
41
+ version="0.1",
42
+ short_description="Camera Traps CNN inference model card",
43
+ full_description="Camera Traps CNN full descr inference model card",
44
+ keywords="cnn, pytorch, icicle",
45
+ author="Joe",
46
+ input_data="",
47
+ input_type="image",
48
+ output_data="",
49
+ category="classification",
50
+ ai_model=self.aimodel,
51
+ bias_analysis=self.bias_analysis,
52
+ xai_analysis=self.xai_analysis
53
+ )
54
+
55
+ model_card.id = model_card._get_hash_id("http://127.0.0.1:5002")
56
+ self.assertEqual(model_card.id, "e6c22bdf9fd3c164a2a9a083fb56fca9328f6ca30f7dcd2ebfc140a7d6f02149")
57
+ print("Success case id:", model_card.id)
58
+
59
+ @patch('requests.get')
60
+ def test_get_hash_id_server_down(self, mock_get):
61
+ """Test hash generation when server is down."""
62
+ # Simulate a server failure
63
+ mock_get.side_effect = requests.exceptions.RequestException("Server is down")
64
+
65
+ model_card = ModelCard(
66
+ name="icicle-camera-traps",
67
+ version="0.1",
68
+ short_description="Camera Traps CNN inference model card",
69
+ full_description="Camera Traps CNN full descr inference model card",
70
+ keywords="cnn, pytorch, icicle",
71
+ author="Joe",
72
+ input_data="",
73
+ input_type="image",
74
+ output_data="",
75
+ category="classification",
76
+ ai_model=self.aimodel,
77
+ bias_analysis=self.bias_analysis,
78
+ xai_analysis=self.xai_analysis
79
+ )
80
+ model_card.id = model_card._get_hash_id("http://127.0.0.1:5002")
81
+
82
+ self.assertIsNotNone(model_card.id)
83
+ print("Server down case id:", model_card.id)
84
+
85
+ @patch('requests.get')
86
+ def test_generate_hash_without_base_url(self, mock_get):
87
+ """Test hash generation when no base URL is provided."""
88
+
89
+ mock_get.assert_not_called()
90
+
91
+ model_card = ModelCard(
92
+ name="icicle-camera-traps",
93
+ version="0.1",
94
+ short_description="Camera Traps CNN inference model card",
95
+ full_description="Camera Traps CNN full descr inference model card",
96
+ keywords="cnn, pytorch, icicle",
97
+ author="Joe",
98
+ input_data="",
99
+ input_type="image",
100
+ output_data="",
101
+ category="classification",
102
+ ai_model=self.aimodel,
103
+ bias_analysis=self.bias_analysis,
104
+ xai_analysis=self.xai_analysis
105
+ )
106
+
107
+ model_card.id = model_card._get_hash_id(None)
108
+ # Generate the expected hash value
109
+ combined_string = f"{model_card.name}:{model_card.version}:{model_card.author}"
110
+ expected_hash = hashlib.sha256(combined_string.encode()).hexdigest()
111
+
112
+ self.assertEqual(model_card.id, expected_hash)
113
+ print("Generated hash id without base_url:", model_card.id)
114
+
115
+ if __name__ == '__main__':
116
+ unittest.main()
@@ -0,0 +1,112 @@
1
+ import os
2
+ import unittest
3
+
4
+ from patra_toolkit import ModelCard, AIModel, BiasAnalysis, ExplainabilityAnalysis, Metric
5
+
6
+ SCHEMA_JSON = os.path.join(os.path.dirname(__file__), os.pardir,
7
+ 'patra_toolkit/schema/schema.json')
8
+
9
+
10
+ class ModelCardTestCase2(unittest.TestCase):
11
+
12
+ def setUp(self) -> None:
13
+ self.aimodel = AIModel(
14
+ name="DenseNet",
15
+ version="v0.1",
16
+ description="DenseNet CNN model",
17
+ owner="PyTorch",
18
+ location="pytorch.org",
19
+ license="testLicence",
20
+ framework="tensorflow",
21
+ model_type="cnn",
22
+ test_accuracy=0.89
23
+ )
24
+ self.bias_analysis = BiasAnalysis(0.1, 0.2)
25
+ self.xai_analysis = ExplainabilityAnalysis("XAI Test 2", [Metric("xai1", 0.5), Metric("xai2", 0.8)])
26
+
27
+ self.mc = ModelCard(
28
+ name="icicle-camera-traps",
29
+ version="0.1",
30
+ short_description="Camera Traps CNN inference model card",
31
+ full_description="Camera Traps CNN full descr inference model card",
32
+ keywords="cnn, pytorch, icicle",
33
+ author="Joe",
34
+ input_data="",
35
+ input_type="image",
36
+ output_data="",
37
+ category="classification",
38
+ ai_model=self.aimodel,
39
+ bias_analysis=self.bias_analysis,
40
+ xai_analysis=self.xai_analysis
41
+ )
42
+ self.mc_save_location = "./test_mc.json"
43
+
44
+
45
+ def test_input_data_empty(self):
46
+ """
47
+ This function tests whether the input data is empty.
48
+ """
49
+ is_valid = self.mc.validate()
50
+ self.assertTrue(is_valid)
51
+
52
+ def test_input_data_doi(self):
53
+ """
54
+ This function tests whether the input data is doi.
55
+ """
56
+ self.mc.input_data = "10.5281/zenodo.11179653"
57
+ is_valid = self.mc.validate()
58
+ self.assertTrue(is_valid)
59
+
60
+ def test_input_data_url(self):
61
+ """
62
+ This function tests whether the input data is url.
63
+ """
64
+ self.mc.input_data = "https://archive.ics.uci.edu/dataset/2/adult"
65
+ is_valid = self.mc.validate()
66
+ self.assertTrue(is_valid)
67
+
68
+ def test_input_data_string(self):
69
+ """
70
+ This function tests whether the input data is string.
71
+ """
72
+ self.mc.input_data = "cifar10"
73
+ is_valid = self.mc.validate()
74
+ self.assertFalse(is_valid)
75
+
76
+ def test_output_data_empty(self):
77
+ """
78
+ This function tests whether the output data is empty.
79
+ """
80
+ self.mc.input_data = "10.5281/zenodo.11179653"
81
+ is_valid = self.mc.validate()
82
+ self.assertTrue(is_valid)
83
+
84
+ def test_output_data_doi(self):
85
+ """
86
+ This function tests whether the output data is doi.
87
+ """
88
+ self.mc.input_data = "10.5281/zenodo.11179653"
89
+ self.mc.output_data = "10.5281/zenodo.11179653"
90
+ is_valid = self.mc.validate()
91
+ self.assertTrue(is_valid)
92
+
93
+ def test_output_data_url(self):
94
+ """
95
+ This function tests whether the output data is url.
96
+ """
97
+ self.mc.input_data = "10.5281/zenodo.11179653"
98
+ self.mc.output_data = "https://archive.ics.uci.edu/dataset/2/adult"
99
+ is_valid = self.mc.validate()
100
+ self.assertTrue(is_valid)
101
+
102
+ def test_output_data_string(self):
103
+ """
104
+ This function tests whether the output data is string.
105
+ """
106
+ self.mc.input_data = "10.5281/zenodo.11179653"
107
+ self.mc.output_data = "hcifar10"
108
+ is_valid = self.mc.validate()
109
+ self.assertFalse(is_valid)
110
+
111
+ if __name__ == '__main__':
112
+ unittest.main()
tests/test_patra_mc.py ADDED
@@ -0,0 +1,159 @@
1
+ import os
2
+ import json
3
+ import unittest
4
+
5
+ from jsonschema import validate
6
+
7
+ from patra_toolkit import ModelCard, AIModel, BiasAnalysis, ExplainabilityAnalysis, Metric, ModelCardJSONEncoder
8
+
9
+ SCHEMA_JSON = os.path.join(os.path.dirname(__file__), os.pardir,
10
+ 'patra_toolkit/schema/schema.json')
11
+
12
+
13
+ class ModelCardTestCase(unittest.TestCase):
14
+
15
+ def setUp(self) -> None:
16
+
17
+ self.aimodel = AIModel(
18
+ name="DenseNet",
19
+ version="v0.1",
20
+ description="DenseNet CNN model",
21
+ owner="PyTorch",
22
+ location="pytorch.org",
23
+ license="testLicence",
24
+ framework="tensorflow",
25
+ model_type="cnn",
26
+ test_accuracy=0.89
27
+ )
28
+ self.bias_analysis = BiasAnalysis(0.1, 0.2)
29
+ self.xai_analysis = ExplainabilityAnalysis("XAI Test 2", [Metric("xai1", 0.5), Metric("xai2", 0.8)])
30
+
31
+ self.mc = ModelCard(
32
+ name="icicle-camera-traps",
33
+ version="0.1",
34
+ short_description="Camera Traps CNN inference model card",
35
+ full_description="Camera Traps CNN full descr inference model card",
36
+ keywords="cnn, pytorch, icicle",
37
+ author="Joe",
38
+ input_data="https://archive.ics.uci.edu/dataset/2/adult",
39
+ input_type="image",
40
+ output_data="https://archive.ics.uci.edu/dataset/2/adult",
41
+ category="classification",
42
+ ai_model=self.aimodel,
43
+ bias_analysis=self.bias_analysis,
44
+ xai_analysis=self.xai_analysis
45
+ )
46
+ self.mc_save_location = "./test_mc.json"
47
+
48
+ def test_json_serialization(self):
49
+ """
50
+ This tests the serialization of the Model Card object.
51
+ :return:
52
+ """
53
+ # Convert the dataclass object to JSON string using the custom encoder
54
+ mc_json = json.dumps(self.mc, cls=ModelCardJSONEncoder, indent=2)
55
+
56
+ # Parse the JSON string back to a dictionary
57
+ parsed_mc = json.loads(mc_json)
58
+
59
+ self.assertEqual(parsed_mc['name'], "icicle-camera-traps")
60
+ self.assertEqual(parsed_mc['ai_model']['name'], "DenseNet")
61
+ self.assertEqual(parsed_mc['bias_analysis']['demographic_parity_difference'], 0.1)
62
+
63
+ def test_schema_compatibility(self):
64
+ """
65
+ This tests whether the python classes are inline with the provided model card schema.
66
+ :return:
67
+ """
68
+ # Convert the dataclass object to JSON string using the custom encoder
69
+ mc_json = json.dumps(self.mc, cls=ModelCardJSONEncoder, indent=4)
70
+ with open(SCHEMA_JSON, 'r') as schema_file:
71
+ schema = json.load(schema_file)
72
+ try:
73
+ validate(json.loads(mc_json), schema)
74
+ except:
75
+ self.fail("Validation Error!")
76
+
77
+ def test_validate(self):
78
+ """
79
+ This tests the validate function.
80
+ :return:
81
+ """
82
+ is_valid = self.mc.validate()
83
+ self.assertTrue(is_valid)
84
+
85
+ def test_framework_enum(self):
86
+ """
87
+ This tests the validate function.
88
+ :return:
89
+ """
90
+ mc = ModelCard(
91
+ name="icicle-camera-traps",
92
+ version="0.1",
93
+ short_description="Camera Traps CNN inference model card",
94
+ full_description="Camera Traps CNN full descr inference model card",
95
+ keywords="cnn, pytorch, icicle",
96
+ author="Joe",
97
+ input_data="https://archive.ics.uci.edu/dataset/2/adult",
98
+ input_type="image",
99
+ output_data="https://archive.ics.uci.edu/dataset/2/adult",
100
+ category="classification"
101
+ )
102
+ aimodel = AIModel(
103
+ name="DenseNet",
104
+ version="v0.1",
105
+ description="DenseNet CNN model",
106
+ owner="PyTorch",
107
+ location="pytorch.org",
108
+ license="testLicence",
109
+ framework="keras",
110
+ model_type="dnn",
111
+ test_accuracy=0.89
112
+ )
113
+ bias_analysis = BiasAnalysis(0.1, 0.2)
114
+ xai_analysis = ExplainabilityAnalysis("XAI Test 2", [Metric("xai1", 0.5), Metric("xai2", 0.8)])
115
+
116
+ mc.ai_model = aimodel
117
+ mc.bias_analysis = bias_analysis
118
+
119
+ is_valid = mc.validate()
120
+ self.assertFalse(is_valid)
121
+
122
+ def test_json_conversion(self):
123
+ """
124
+ This tests data insertion via converting it to json, converting back to python.
125
+ :return:
126
+ """
127
+
128
+ # Convert the dataclass object to JSON string using the custom encoder
129
+ mc_json = json.dumps(self.mc, cls=ModelCardJSONEncoder, indent=4)
130
+
131
+ # convert the json string back to python model
132
+ mc_converted = json.loads(mc_json)
133
+
134
+ self.assertEqual(mc_converted['input_data'], "https://archive.ics.uci.edu/dataset/2/adult")
135
+
136
+ def test_json_save(self):
137
+ """
138
+ This tests if the MC save as json works
139
+ :return:
140
+ """
141
+ self.mc.save(self.mc_save_location)
142
+
143
+ # file existing verification
144
+ self.assertTrue(os.path.exists(self.mc_save_location), "File not found")
145
+
146
+ # Verify the content in the file is same as the saved data
147
+ with open(self.mc_save_location, 'r') as json_file:
148
+ saved_data = json.load(json_file)
149
+
150
+ self.assertEqual(saved_data['name'], self.mc.name, "Saved JSON data doesn't match the original Model Card")
151
+
152
+ def tearDown(self):
153
+ # Removing the saved file for the model card
154
+ if os.path.exists(self.mc_save_location):
155
+ os.remove(self.mc_save_location)
156
+
157
+
158
+ if __name__ == '__main__':
159
+ unittest.main()