torch-molecule 0.1.0.dev2__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 (148) hide show
  1. torch_molecule/__init__.py +70 -0
  2. torch_molecule/base/__init__.py +8 -0
  3. torch_molecule/base/base.py +396 -0
  4. torch_molecule/base/encoder.py +44 -0
  5. torch_molecule/base/generator.py +24 -0
  6. torch_molecule/base/old_encoder.py +322 -0
  7. torch_molecule/base/old_predictor.py +904 -0
  8. torch_molecule/base/old_predictor2.py +436 -0
  9. torch_molecule/base/predictor.py +112 -0
  10. torch_molecule/encoder/attrmask/__init__.py +3 -0
  11. torch_molecule/encoder/attrmask/model.py +113 -0
  12. torch_molecule/encoder/attrmask/modeling_attrmask.py +304 -0
  13. torch_molecule/encoder/constant.py +13 -0
  14. torch_molecule/encoder/contextpred/__init__.py +3 -0
  15. torch_molecule/encoder/contextpred/model.py +207 -0
  16. torch_molecule/encoder/contextpred/modeling_contextpred.py +299 -0
  17. torch_molecule/encoder/contextpred/utils.py +206 -0
  18. torch_molecule/encoder/edgepred/__init__.py +3 -0
  19. torch_molecule/encoder/edgepred/model.py +104 -0
  20. torch_molecule/encoder/edgepred/modeling_edgepred.py +285 -0
  21. torch_molecule/encoder/graphmae/__init__.py +3 -0
  22. torch_molecule/encoder/graphmae/dataloader.py +206 -0
  23. torch_molecule/encoder/graphmae/model.py +137 -0
  24. torch_molecule/encoder/graphmae/modeling_graphmae.py +323 -0
  25. torch_molecule/encoder/infograph/__init__.py +3 -0
  26. torch_molecule/encoder/infograph/loss_function.py +128 -0
  27. torch_molecule/encoder/infograph/model.py +92 -0
  28. torch_molecule/encoder/infograph/modeling_infograph.py +301 -0
  29. torch_molecule/encoder/infograph/module.py +31 -0
  30. torch_molecule/encoder/moama/__init__.py +3 -0
  31. torch_molecule/encoder/moama/model.py +131 -0
  32. torch_molecule/encoder/moama/modeling_moama.py +307 -0
  33. torch_molecule/encoder/moama/utils.py +143 -0
  34. torch_molecule/encoder/pretrained/__init__.py +3 -0
  35. torch_molecule/encoder/pretrained/modeling_pretrained.py +274 -0
  36. torch_molecule/encoder/supervised/__init__.py +3 -0
  37. torch_molecule/encoder/supervised/model.py +97 -0
  38. torch_molecule/encoder/supervised/modeling_supervised.py +354 -0
  39. torch_molecule/generator/digress/__init__.py +3 -0
  40. torch_molecule/generator/digress/diffusion.py +194 -0
  41. torch_molecule/generator/digress/modeling_digress.py +503 -0
  42. torch_molecule/generator/digress/transformer.py +360 -0
  43. torch_molecule/generator/digress/utils.py +196 -0
  44. torch_molecule/generator/gdss/__init__.py +3 -0
  45. torch_molecule/generator/gdss/layers.py +257 -0
  46. torch_molecule/generator/gdss/model.py +204 -0
  47. torch_molecule/generator/gdss/modeling_gdss.py +474 -0
  48. torch_molecule/generator/gdss/sde.py +334 -0
  49. torch_molecule/generator/gdss/solver.py +227 -0
  50. torch_molecule/generator/gdss/toremove/ScoreNetworkA.py +167 -0
  51. torch_molecule/generator/gdss/toremove/ScoreNetworkX.py +85 -0
  52. torch_molecule/generator/gdss/utils.py +240 -0
  53. torch_molecule/generator/graph_dit/__init__.py +3 -0
  54. torch_molecule/generator/graph_dit/diffusion.py +188 -0
  55. torch_molecule/generator/graph_dit/modeling_graph_dit.py +581 -0
  56. torch_molecule/generator/graph_dit/transformer.py +207 -0
  57. torch_molecule/generator/graph_dit/utils.py +220 -0
  58. torch_molecule/generator/graph_ga/__init__.py +3 -0
  59. torch_molecule/generator/graph_ga/crossover.py +182 -0
  60. torch_molecule/generator/graph_ga/modeling_graph_ga.py +310 -0
  61. torch_molecule/generator/graph_ga/mutate.py +136 -0
  62. torch_molecule/generator/graph_ga/oracle.py +95 -0
  63. torch_molecule/generator/jtvae/__init__.py +3 -0
  64. torch_molecule/generator/jtvae/jtnn/chemutils.py +450 -0
  65. torch_molecule/generator/jtvae/jtnn/datautils.py +127 -0
  66. torch_molecule/generator/jtvae/jtnn/jtmpn.py +140 -0
  67. torch_molecule/generator/jtvae/jtnn/jtnn_dec.py +350 -0
  68. torch_molecule/generator/jtvae/jtnn/jtnn_enc.py +131 -0
  69. torch_molecule/generator/jtvae/jtnn/mol_tree.py +111 -0
  70. torch_molecule/generator/jtvae/jtnn/mpn.py +129 -0
  71. torch_molecule/generator/jtvae/jtnn/nnutils.py +66 -0
  72. torch_molecule/generator/jtvae/jtnn/vocab.py +31 -0
  73. torch_molecule/generator/jtvae/jtnn_vae.py +246 -0
  74. torch_molecule/generator/jtvae/modeling_jtvae.py +247 -0
  75. torch_molecule/generator/lstm/__init__.py +3 -0
  76. torch_molecule/generator/lstm/action_sampler.py +91 -0
  77. torch_molecule/generator/lstm/lstm.py +59 -0
  78. torch_molecule/generator/lstm/modeling_lstm.py +263 -0
  79. torch_molecule/generator/lstm/smiles_char_dict.py +123 -0
  80. torch_molecule/generator/lstm/utils.py +62 -0
  81. torch_molecule/generator/molgpt/__init__.py +3 -0
  82. torch_molecule/generator/molgpt/dataset.py +87 -0
  83. torch_molecule/generator/molgpt/gpt.py +251 -0
  84. torch_molecule/generator/molgpt/modeling_molgpt.py +427 -0
  85. torch_molecule/generator/pretrained/modeling_pretrained.py +1 -0
  86. torch_molecule/nn/__init__.py +16 -0
  87. torch_molecule/nn/attention.py +63 -0
  88. torch_molecule/nn/embedder.py +183 -0
  89. torch_molecule/nn/gnn.py +631 -0
  90. torch_molecule/nn/mlp.py +34 -0
  91. torch_molecule/predictor/bfgnn/__init__.py +3 -0
  92. torch_molecule/predictor/bfgnn/model.py +116 -0
  93. torch_molecule/predictor/bfgnn/modeling_bfgnn.py +81 -0
  94. torch_molecule/predictor/dir/__init__.py +3 -0
  95. torch_molecule/predictor/dir/model.py +235 -0
  96. torch_molecule/predictor/dir/modeling_dir.py +161 -0
  97. torch_molecule/predictor/dir/utils.py +40 -0
  98. torch_molecule/predictor/gnn/__init__.py +3 -0
  99. torch_molecule/predictor/gnn/model.py +114 -0
  100. torch_molecule/predictor/gnn/modeling_gnn.py +706 -0
  101. torch_molecule/predictor/gnn/old_modeling_gnn.py +707 -0
  102. torch_molecule/predictor/grea/__init__.py +3 -0
  103. torch_molecule/predictor/grea/model.py +190 -0
  104. torch_molecule/predictor/grea/modeling_grea.py +107 -0
  105. torch_molecule/predictor/grea/old1_features.py +229 -0
  106. torch_molecule/predictor/grea/old1_modeling_grea.py +493 -0
  107. torch_molecule/predictor/grea/utils.py +97 -0
  108. torch_molecule/predictor/grin/__init__.py +3 -0
  109. torch_molecule/predictor/grin/model.py +116 -0
  110. torch_molecule/predictor/grin/modeling_grin.py +91 -0
  111. torch_molecule/predictor/irm/__init__.py +3 -0
  112. torch_molecule/predictor/irm/model.py +127 -0
  113. torch_molecule/predictor/irm/modeling_irm.py +179 -0
  114. torch_molecule/predictor/lstm/__init__.py +3 -0
  115. torch_molecule/predictor/lstm/model.py +145 -0
  116. torch_molecule/predictor/lstm/modeling_lstm.py +627 -0
  117. torch_molecule/predictor/lstm/token_from_smiles.py +85 -0
  118. torch_molecule/predictor/old_modeling_graph.py +650 -0
  119. torch_molecule/predictor/rpgnn/__init__.py +3 -0
  120. torch_molecule/predictor/rpgnn/model.py +185 -0
  121. torch_molecule/predictor/rpgnn/modeling_rpgnn.py +76 -0
  122. torch_molecule/predictor/sgir/__init__.py +3 -0
  123. torch_molecule/predictor/sgir/modeling_sgir.py +260 -0
  124. torch_molecule/predictor/sgir/strategy.py +266 -0
  125. torch_molecule/predictor/smiles_transformer/__init__.py +3 -0
  126. torch_molecule/predictor/smiles_transformer/model.py +424 -0
  127. torch_molecule/predictor/smiles_transformer/modeling_transformer.py +252 -0
  128. torch_molecule/predictor/ssr/__init__.py +3 -0
  129. torch_molecule/predictor/ssr/model.py +283 -0
  130. torch_molecule/predictor/ssr/modeling_ssr.py +267 -0
  131. torch_molecule/predictor/ssr/utils.py +185 -0
  132. torch_molecule/utils/__init__.py +31 -0
  133. torch_molecule/utils/checker.py +158 -0
  134. torch_molecule/utils/checkpoint.py +294 -0
  135. torch_molecule/utils/format.py +140 -0
  136. torch_molecule/utils/generic/metrics.py +530 -0
  137. torch_molecule/utils/generic/pseudo_tasks.py +11 -0
  138. torch_molecule/utils/generic/weights.py +30 -0
  139. torch_molecule/utils/graph/features.py +178 -0
  140. torch_molecule/utils/graph/graph_from_smiles.py +134 -0
  141. torch_molecule/utils/graph/graph_to_smiles.py +312 -0
  142. torch_molecule/utils/hf.py +232 -0
  143. torch_molecule/utils/search.py +64 -0
  144. torch_molecule-0.1.0.dev2.dist-info/METADATA +243 -0
  145. torch_molecule-0.1.0.dev2.dist-info/RECORD +148 -0
  146. torch_molecule-0.1.0.dev2.dist-info/WHEEL +5 -0
  147. torch_molecule-0.1.0.dev2.dist-info/licenses/LICENSE +21 -0
  148. torch_molecule-0.1.0.dev2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,294 @@
1
+ import os
2
+ import json
3
+ import datetime
4
+ import tempfile
5
+ import torch
6
+ import warnings
7
+ import numpy as np
8
+
9
+ # Import typing dependencies if needed
10
+ from typing import Optional, Dict, Any
11
+
12
+ HF_METADATA = {
13
+ "tags": [
14
+ "torch_molecule",
15
+ "molecular-property-prediction",
16
+ ],
17
+ "library_name": "torch_molecule",
18
+ }
19
+ from ..utils.format import sanitize_config
20
+
21
+ class LocalCheckpointManager:
22
+ """Handles saving and loading of models to and from local paths."""
23
+
24
+ @staticmethod
25
+ def save_model_to_local(model_instance, path: str) -> None:
26
+ """Save model weights and configuration to a local file."""
27
+ if not model_instance.is_fitted_:
28
+ raise ValueError("Model must be fitted before saving.")
29
+
30
+ if not path.endswith((".pt", ".pth")):
31
+ raise ValueError("Save path should end with '.pt' or '.pth'.")
32
+
33
+ os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
34
+ model_name = os.path.splitext(os.path.basename(path))[0]
35
+
36
+ save_dict = {
37
+ "model_state_dict": model_instance.model.state_dict(),
38
+ "hyperparameters": model_instance.get_params(),
39
+ "model_name": model_name,
40
+ "date_saved": datetime.datetime.now().isoformat(),
41
+ "version": getattr(model_instance, "__version__", "1.0.0"),
42
+ }
43
+ torch.save(save_dict, path)
44
+
45
+ @staticmethod
46
+ def load_model_from_local(model_instance, path: str) -> None:
47
+ """Load model weights and configuration from a local file."""
48
+ if not os.path.exists(path):
49
+ raise FileNotFoundError(f"No model file found at '{path}'.")
50
+
51
+ try:
52
+ checkpoint = torch.load(path, map_location=model_instance.device, weights_only=False)
53
+ except Exception as e:
54
+ raise ValueError(f"Error loading model from {path}: {str(e)}")
55
+
56
+ verbose = model_instance.get_params().get("verbose", False)
57
+
58
+ required_keys = {"model_state_dict", "hyperparameters", "model_name"}
59
+ if not all(key in checkpoint for key in required_keys):
60
+ missing_keys = required_keys - set(checkpoint.keys())
61
+ raise ValueError(f"Checkpoint missing required keys: {missing_keys}")
62
+
63
+ parameter_status = []
64
+ for key, new_value in checkpoint["hyperparameters"].items():
65
+ if hasattr(model_instance, key):
66
+ old_value = getattr(model_instance, key)
67
+ is_changed = (old_value != new_value)
68
+ parameter_status.append({
69
+ "Parameter": key,
70
+ "Old Value": old_value,
71
+ "New Value": new_value,
72
+ "Status": "Changed" if is_changed else "Unchanged",
73
+ })
74
+ if is_changed:
75
+ setattr(model_instance, key, new_value)
76
+
77
+ if parameter_status and verbose:
78
+ print("\nHyperparameter Status:")
79
+ print("-" * 80)
80
+ print(f"{'Parameter':<20} {'Old Value':<20} {'New Value':<20} {'Status':<10}")
81
+ print("-" * 80)
82
+ # Sort so that changed parameters appear first
83
+ parameter_status.sort(key=lambda x: (x["Status"] != "Changed", x["Parameter"]))
84
+ for param in parameter_status:
85
+ color = "\033[91m" if param["Status"] == "Changed" else "\033[92m"
86
+ print(
87
+ f"{param['Parameter']:<20} "
88
+ f"{str(param['Old Value']):<20} "
89
+ f"{str(param['New Value']):<20} "
90
+ f"{color}{param['Status']}\033[0m"
91
+ )
92
+ print("-" * 80)
93
+ changes_count = sum(1 for p in parameter_status if p["Status"] == "Changed")
94
+ print(
95
+ f"\nSummary: {changes_count} parameters changed, "
96
+ f"{len(parameter_status) - changes_count} unchanged"
97
+ )
98
+
99
+ # Reinitialize
100
+ model_instance._initialize_model(model_instance.model_class, checkpoint)
101
+ model_instance.model_name = checkpoint["model_name"]
102
+ model_instance.is_fitted_ = True
103
+ model_instance.model = model_instance.model.to(model_instance.device)
104
+ print(f"Model successfully loaded from local path: {path}")
105
+
106
+ class HuggingFaceCheckpointManager:
107
+ """Handles saving and loading of models to and from the Hugging Face Hub."""
108
+
109
+ @staticmethod
110
+ def load_model_from_hf(model_instance, repo_id: str, path: str, config_filename: str = "config.json") -> None:
111
+ """Load model from Hugging Face Hub, saving locally to `path` first."""
112
+ try:
113
+ from huggingface_hub import hf_hub_download
114
+ except ImportError:
115
+ raise ImportError(
116
+ "huggingface_hub package is required to load from Hugging Face Hub. "
117
+ "Install it with: pip install huggingface_hub"
118
+ )
119
+
120
+ try:
121
+ os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
122
+ model_name = os.path.splitext(os.path.basename(path))[0]
123
+
124
+ downloaded_path = hf_hub_download(
125
+ repo_id=repo_id,
126
+ filename=f"{model_name}.pt",
127
+ local_dir=os.path.dirname(path),
128
+ )
129
+
130
+ hf_hub_download(
131
+ repo_id=repo_id,
132
+ filename=config_filename,
133
+ local_dir=os.path.dirname(path),
134
+ )
135
+
136
+ checkpoint = torch.load(downloaded_path, map_location=model_instance.device, weights_only=False)
137
+
138
+ required_keys = {"model_state_dict", "hyperparameters", "model_name"}
139
+ if not all(key in checkpoint for key in required_keys):
140
+ missing_keys = required_keys - set(checkpoint.keys())
141
+ raise ValueError(f"Checkpoint missing required keys: {missing_keys}")
142
+
143
+ parameter_status = []
144
+ for key, new_value in checkpoint["hyperparameters"].items():
145
+ if hasattr(model_instance, key):
146
+ old_value = getattr(model_instance, key)
147
+ is_changed = (old_value != new_value)
148
+ parameter_status.append({
149
+ "Parameter": key,
150
+ "Old Value": old_value,
151
+ "New Value": new_value,
152
+ "Status": "Changed" if is_changed else "Unchanged",
153
+ })
154
+ if is_changed:
155
+ setattr(model_instance, key, new_value)
156
+
157
+ if parameter_status:
158
+ print("\nHyperparameter Status:")
159
+ print("-" * 80)
160
+ print(f"{'Parameter':<20} {'Old Value':<20} {'New Value':<20} {'Status':<10}")
161
+ print("-" * 80)
162
+ parameter_status.sort(key=lambda x: (x["Status"] != "Changed", x["Parameter"]))
163
+ for param in parameter_status:
164
+ color = "\033[91m" if param["Status"] == "Changed" else "\033[92m"
165
+ print(
166
+ f"{param['Parameter']:<20} "
167
+ f"{str(param['Old Value']):<20} "
168
+ f"{str(param['New Value']):<20} "
169
+ f"{color}{param['Status']}\033[0m"
170
+ )
171
+ print("-" * 80)
172
+ changes_count = sum(1 for p in parameter_status if p["Status"] == "Changed")
173
+ print(
174
+ f"\nSummary: {changes_count} parameters changed, "
175
+ f"{len(parameter_status) - changes_count} unchanged"
176
+ )
177
+
178
+ model_instance._initialize_model(model_instance.model_class, checkpoint)
179
+ model_instance.model_name = checkpoint["model_name"]
180
+ model_instance.is_fitted_ = True
181
+ model_instance.model = model_instance.model.to(model_instance.device)
182
+ print(f"Model successfully loaded from repository {repo_id}")
183
+
184
+ except Exception as e:
185
+ if os.path.exists(path):
186
+ os.remove(path) # Clean up partial downloads
187
+ raise RuntimeError(f"Failed to download or load model from repository '{repo_id}': {str(e)}")
188
+
189
+ @staticmethod
190
+ def push_to_huggingface(
191
+ model_instance,
192
+ repo_id: str,
193
+ task_id: str = "default",
194
+ metadata_dict: Optional[Dict[str, Any]] = None,
195
+ metrics: Optional[Dict[str, float]] = None,
196
+ commit_message: str = "Update model",
197
+ token: Optional[str] = None,
198
+ private: bool = False,
199
+ config_filename: str = "config.json",
200
+ ) -> None:
201
+ """Push a task-specific model checkpoint to Hugging Face Hub."""
202
+ try:
203
+ from huggingface_hub import HfApi, create_repo, metadata_update
204
+ from .hf import merge_task_configs, get_existing_repo_data, create_model_card
205
+ except ImportError:
206
+ raise ImportError(
207
+ "huggingface_hub package is required to push to Hugging Face Hub. "
208
+ "Install it with: pip install huggingface_hub"
209
+ )
210
+
211
+ if not isinstance(repo_id, str) or "/" not in repo_id:
212
+ raise ValueError("repo_id must be in format '<username>/<model_name>'.")
213
+ if not task_id:
214
+ raise ValueError("task_id must be provided.")
215
+ if not model_instance.is_fitted_:
216
+ raise ValueError("Model must be fitted before pushing to Hugging Face Hub.")
217
+
218
+ try:
219
+ api = HfApi(token=token)
220
+ repo_exists, existing_config, existing_readme = get_existing_repo_data(repo_id, token)
221
+ create_repo(repo_id, private=private, token=token, exist_ok=True)
222
+
223
+ with tempfile.TemporaryDirectory() as tmp_dir:
224
+ local_path = os.path.join(tmp_dir, f"{model_instance.model_name}.pt")
225
+ # Use the local manager to save a copy
226
+ LocalCheckpointManager.save_model_to_local(model_instance, local_path)
227
+
228
+ # Prepare task-specific config
229
+ task_config = {
230
+ **({"task_type": model_instance.task_type} if hasattr(model_instance, "task_type") else {}),
231
+ "config": sanitize_config(model_instance.get_params(deep=True)),
232
+ "metrics": metrics or {},
233
+ }
234
+
235
+ # Merge with any existing config
236
+ base_config = (
237
+ {
238
+ "model_name": model_instance.model_name,
239
+ "framework": "torch_molecule",
240
+ "date_created": existing_config.get(
241
+ "date_created", datetime.datetime.now().isoformat()
242
+ ),
243
+ }
244
+ if not repo_exists
245
+ else existing_config
246
+ )
247
+
248
+ num_params = sum(p.numel() for p in model_instance.model.parameters())
249
+ final_config = merge_task_configs(
250
+ task_id=task_id,
251
+ existing_config=base_config,
252
+ new_task_config=task_config,
253
+ num_params=num_params,
254
+ )
255
+
256
+ # Save config file
257
+ config_path = os.path.join(tmp_dir, config_filename)
258
+ with open(config_path, "w") as f:
259
+ json.dump(final_config, f, indent=2)
260
+
261
+ # Create model card
262
+ readme_content = create_model_card(
263
+ model_class=model_instance.__class__.__name__,
264
+ model_name=model_instance.model_name,
265
+ tasks_config=final_config.get("tasks", {}),
266
+ model_config=final_config,
267
+ repo_id=repo_id,
268
+ existing_readme=existing_readme,
269
+ )
270
+ readme_path = os.path.join(tmp_dir, "README.md")
271
+ with open(readme_path, "w") as f:
272
+ f.write(readme_content)
273
+
274
+ # Upload everything
275
+ api.upload_folder(
276
+ repo_id=repo_id,
277
+ folder_path=tmp_dir,
278
+ commit_message=f"{commit_message} - Task: {task_id}",
279
+ )
280
+
281
+ # Update or add repository metadata
282
+ metadata_dict = HF_METADATA if metadata_dict is None else metadata_dict
283
+ metadata_update(repo_id=repo_id, metadata=metadata_dict, token=token, overwrite=True)
284
+
285
+ # Print summary
286
+ task_info = final_config["tasks"][task_id]
287
+ print(f"Successfully pushed model for task {task_id} to {repo_id}")
288
+ print(f"Task version: {task_info['current_version']}")
289
+ if metrics:
290
+ print("Metrics:")
291
+ for metric, value in metrics.items():
292
+ print(f" {metric}: {value:.4f}")
293
+ except Exception as e:
294
+ raise RuntimeError(f"Failed to push to Hugging Face Hub: {str(e)}")
@@ -0,0 +1,140 @@
1
+ import torch
2
+ import numpy as np
3
+ import types
4
+ import inspect
5
+
6
+ def serialize_config(obj):
7
+ """Helper function to make config JSON serializable.
8
+
9
+ Handles special cases like lambda functions, torch modules, and numpy arrays.
10
+
11
+ Parameters
12
+ ----------
13
+ obj : Any
14
+ The object to serialize
15
+
16
+ Returns
17
+ -------
18
+ Any
19
+ JSON serializable representation of the object
20
+ """
21
+ # Handle None
22
+ if obj is None:
23
+ return None
24
+
25
+ # Handle lambda and regular functions
26
+ if isinstance(obj, (types.LambdaType, types.FunctionType, types.MethodType)):
27
+ # For lambda functions, try to get the source code
28
+ try:
29
+ if isinstance(obj, types.LambdaType):
30
+ source = inspect.getsource(obj).strip()
31
+ return {"_type": "lambda", "source": source}
32
+ else:
33
+ return {"_type": "function", "name": obj.__name__}
34
+ except (IOError, TypeError):
35
+ # Fallback for built-in functions or when source isn't available
36
+ return {"_type": "function", "name": str(obj)}
37
+
38
+ # Handle PyTorch modules and optimizers
39
+ elif isinstance(obj, (torch.nn.Module, torch.optim.Optimizer)):
40
+ return {
41
+ "_type": "torch_class",
42
+ "class_name": obj.__class__.__name__,
43
+ "module": obj.__class__.__module__
44
+ }
45
+
46
+ # Handle PyTorch tensors
47
+ elif isinstance(obj, torch.Tensor):
48
+ # If it's a single number wrapped in a torch tensor, just return the number
49
+ if obj.numel() == 1:
50
+ return obj.item()
51
+ elif obj.numel() < 1000:
52
+ return {
53
+ "_type": "torch_tensor",
54
+ "data": obj.detach().cpu().tolist(),
55
+ "shape": list(obj.shape)
56
+ }
57
+ return {
58
+ "_type": "torch_tensor",
59
+ "shape": list(obj.shape)
60
+ }
61
+
62
+ # Handle numpy arrays
63
+ elif isinstance(obj, (np.ndarray, np.generic)):
64
+ # If it's a single number wrapped in a numpy array, just return the number
65
+ if obj.size == 1:
66
+ return obj.item()
67
+ elif obj.size < 1000:
68
+ return {
69
+ "_type": "numpy_array",
70
+ "data": obj.tolist(),
71
+ "shape": list(obj.shape)
72
+ }
73
+ return {
74
+ "_type": "numpy_array",
75
+ "shape": list(obj.shape)
76
+ }
77
+
78
+ # Handle sets and frozensets
79
+ elif isinstance(obj, (set, frozenset)):
80
+ return {
81
+ "_type": "set",
82
+ "data": list(obj)
83
+ }
84
+
85
+ # Handle custom objects with __dict__
86
+ elif hasattr(obj, '__dict__'):
87
+ return {
88
+ "_type": "custom_class",
89
+ "class_name": obj.__class__.__name__,
90
+ "module": obj.__class__.__module__
91
+ }
92
+
93
+ # Handle basic types that are JSON serializable
94
+ elif isinstance(obj, (str, int, float, bool)):
95
+ return obj
96
+
97
+ # Handle any other types by converting to string
98
+ return {
99
+ "_type": "unknown",
100
+ "repr": str(obj)
101
+ }
102
+
103
+
104
+ def sanitize_config(config_dict):
105
+ """Recursively sanitize config dictionary for JSON serialization.
106
+
107
+ Handles nested structures and special cases.
108
+
109
+ Parameters
110
+ ----------
111
+ config_dict : dict
112
+ Configuration dictionary to sanitize
113
+
114
+ Returns
115
+ -------
116
+ dict
117
+ Sanitized configuration dictionary that is JSON serializable
118
+ """
119
+ if not isinstance(config_dict, dict):
120
+ return serialize_config(config_dict)
121
+
122
+ clean_dict = {}
123
+ for key, value in config_dict.items():
124
+ # Skip private attributes and callable objects stored as attributes
125
+ if isinstance(key, str) and (key.startswith('_') or callable(value)):
126
+ continue
127
+
128
+ # Handle nested dictionaries
129
+ if isinstance(value, dict):
130
+ clean_dict[key] = sanitize_config(value)
131
+
132
+ # Handle lists and tuples
133
+ elif isinstance(value, (list, tuple)):
134
+ clean_dict[key] = [sanitize_config(v) for v in value]
135
+
136
+ # Handle all other types
137
+ else:
138
+ clean_dict[key] = serialize_config(value)
139
+
140
+ return clean_dict