morphml 1.0.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.

Potentially problematic release.


This version of morphml might be problematic. Click here for more details.

Files changed (158) hide show
  1. morphml/__init__.py +14 -0
  2. morphml/api/__init__.py +26 -0
  3. morphml/api/app.py +326 -0
  4. morphml/api/auth.py +193 -0
  5. morphml/api/client.py +338 -0
  6. morphml/api/models.py +132 -0
  7. morphml/api/rate_limit.py +192 -0
  8. morphml/benchmarking/__init__.py +36 -0
  9. morphml/benchmarking/comparison.py +430 -0
  10. morphml/benchmarks/__init__.py +56 -0
  11. morphml/benchmarks/comparator.py +409 -0
  12. morphml/benchmarks/datasets.py +280 -0
  13. morphml/benchmarks/metrics.py +199 -0
  14. morphml/benchmarks/openml_suite.py +201 -0
  15. morphml/benchmarks/problems.py +289 -0
  16. morphml/benchmarks/suite.py +318 -0
  17. morphml/cli/__init__.py +5 -0
  18. morphml/cli/commands/experiment.py +329 -0
  19. morphml/cli/main.py +457 -0
  20. morphml/cli/quickstart.py +312 -0
  21. morphml/config.py +278 -0
  22. morphml/constraints/__init__.py +19 -0
  23. morphml/constraints/handler.py +205 -0
  24. morphml/constraints/predicates.py +285 -0
  25. morphml/core/__init__.py +3 -0
  26. morphml/core/crossover.py +449 -0
  27. morphml/core/dsl/README.md +359 -0
  28. morphml/core/dsl/__init__.py +72 -0
  29. morphml/core/dsl/ast_nodes.py +364 -0
  30. morphml/core/dsl/compiler.py +318 -0
  31. morphml/core/dsl/layers.py +368 -0
  32. morphml/core/dsl/lexer.py +336 -0
  33. morphml/core/dsl/parser.py +455 -0
  34. morphml/core/dsl/search_space.py +386 -0
  35. morphml/core/dsl/syntax.py +199 -0
  36. morphml/core/dsl/type_system.py +361 -0
  37. morphml/core/dsl/validator.py +386 -0
  38. morphml/core/graph/__init__.py +40 -0
  39. morphml/core/graph/edge.py +124 -0
  40. morphml/core/graph/graph.py +507 -0
  41. morphml/core/graph/mutations.py +409 -0
  42. morphml/core/graph/node.py +196 -0
  43. morphml/core/graph/serialization.py +361 -0
  44. morphml/core/graph/visualization.py +431 -0
  45. morphml/core/objectives/__init__.py +20 -0
  46. morphml/core/search/__init__.py +33 -0
  47. morphml/core/search/individual.py +252 -0
  48. morphml/core/search/parameters.py +453 -0
  49. morphml/core/search/population.py +375 -0
  50. morphml/core/search/search_engine.py +340 -0
  51. morphml/distributed/__init__.py +76 -0
  52. morphml/distributed/fault_tolerance.py +497 -0
  53. morphml/distributed/health_monitor.py +348 -0
  54. morphml/distributed/master.py +709 -0
  55. morphml/distributed/proto/README.md +224 -0
  56. morphml/distributed/proto/__init__.py +74 -0
  57. morphml/distributed/proto/worker.proto +170 -0
  58. morphml/distributed/proto/worker_pb2.py +79 -0
  59. morphml/distributed/proto/worker_pb2_grpc.py +423 -0
  60. morphml/distributed/resource_manager.py +416 -0
  61. morphml/distributed/scheduler.py +567 -0
  62. morphml/distributed/storage/__init__.py +33 -0
  63. morphml/distributed/storage/artifacts.py +381 -0
  64. morphml/distributed/storage/cache.py +366 -0
  65. morphml/distributed/storage/checkpointing.py +329 -0
  66. morphml/distributed/storage/database.py +459 -0
  67. morphml/distributed/worker.py +549 -0
  68. morphml/evaluation/__init__.py +5 -0
  69. morphml/evaluation/heuristic.py +237 -0
  70. morphml/exceptions.py +55 -0
  71. morphml/execution/__init__.py +5 -0
  72. morphml/execution/local_executor.py +350 -0
  73. morphml/integrations/__init__.py +28 -0
  74. morphml/integrations/jax_adapter.py +206 -0
  75. morphml/integrations/pytorch_adapter.py +530 -0
  76. morphml/integrations/sklearn_adapter.py +206 -0
  77. morphml/integrations/tensorflow_adapter.py +230 -0
  78. morphml/logging_config.py +93 -0
  79. morphml/meta_learning/__init__.py +66 -0
  80. morphml/meta_learning/architecture_similarity.py +277 -0
  81. morphml/meta_learning/experiment_database.py +240 -0
  82. morphml/meta_learning/knowledge_base/__init__.py +19 -0
  83. morphml/meta_learning/knowledge_base/embedder.py +179 -0
  84. morphml/meta_learning/knowledge_base/knowledge_base.py +313 -0
  85. morphml/meta_learning/knowledge_base/meta_features.py +265 -0
  86. morphml/meta_learning/knowledge_base/vector_store.py +271 -0
  87. morphml/meta_learning/predictors/__init__.py +27 -0
  88. morphml/meta_learning/predictors/ensemble.py +221 -0
  89. morphml/meta_learning/predictors/gnn_predictor.py +552 -0
  90. morphml/meta_learning/predictors/learning_curve.py +231 -0
  91. morphml/meta_learning/predictors/proxy_metrics.py +261 -0
  92. morphml/meta_learning/strategy_evolution/__init__.py +27 -0
  93. morphml/meta_learning/strategy_evolution/adaptive_optimizer.py +226 -0
  94. morphml/meta_learning/strategy_evolution/bandit.py +276 -0
  95. morphml/meta_learning/strategy_evolution/portfolio.py +230 -0
  96. morphml/meta_learning/transfer.py +581 -0
  97. morphml/meta_learning/warm_start.py +286 -0
  98. morphml/optimizers/__init__.py +74 -0
  99. morphml/optimizers/adaptive_operators.py +399 -0
  100. morphml/optimizers/bayesian/__init__.py +52 -0
  101. morphml/optimizers/bayesian/acquisition.py +387 -0
  102. morphml/optimizers/bayesian/base.py +319 -0
  103. morphml/optimizers/bayesian/gaussian_process.py +635 -0
  104. morphml/optimizers/bayesian/smac.py +534 -0
  105. morphml/optimizers/bayesian/tpe.py +411 -0
  106. morphml/optimizers/differential_evolution.py +220 -0
  107. morphml/optimizers/evolutionary/__init__.py +61 -0
  108. morphml/optimizers/evolutionary/cma_es.py +416 -0
  109. morphml/optimizers/evolutionary/differential_evolution.py +556 -0
  110. morphml/optimizers/evolutionary/encoding.py +426 -0
  111. morphml/optimizers/evolutionary/particle_swarm.py +449 -0
  112. morphml/optimizers/genetic_algorithm.py +486 -0
  113. morphml/optimizers/gradient_based/__init__.py +22 -0
  114. morphml/optimizers/gradient_based/darts.py +550 -0
  115. morphml/optimizers/gradient_based/enas.py +585 -0
  116. morphml/optimizers/gradient_based/operations.py +474 -0
  117. morphml/optimizers/gradient_based/utils.py +601 -0
  118. morphml/optimizers/hill_climbing.py +169 -0
  119. morphml/optimizers/multi_objective/__init__.py +56 -0
  120. morphml/optimizers/multi_objective/indicators.py +504 -0
  121. morphml/optimizers/multi_objective/nsga2.py +647 -0
  122. morphml/optimizers/multi_objective/visualization.py +427 -0
  123. morphml/optimizers/nsga2.py +308 -0
  124. morphml/optimizers/random_search.py +172 -0
  125. morphml/optimizers/simulated_annealing.py +181 -0
  126. morphml/plugins/__init__.py +35 -0
  127. morphml/plugins/custom_evaluator_example.py +81 -0
  128. morphml/plugins/custom_optimizer_example.py +63 -0
  129. morphml/plugins/plugin_system.py +454 -0
  130. morphml/reports/__init__.py +30 -0
  131. morphml/reports/generator.py +362 -0
  132. morphml/tracking/__init__.py +7 -0
  133. morphml/tracking/experiment.py +309 -0
  134. morphml/tracking/logger.py +301 -0
  135. morphml/tracking/reporter.py +357 -0
  136. morphml/utils/__init__.py +6 -0
  137. morphml/utils/checkpoint.py +189 -0
  138. morphml/utils/comparison.py +390 -0
  139. morphml/utils/export.py +407 -0
  140. morphml/utils/progress.py +392 -0
  141. morphml/utils/validation.py +392 -0
  142. morphml/version.py +7 -0
  143. morphml/visualization/__init__.py +50 -0
  144. morphml/visualization/analytics.py +423 -0
  145. morphml/visualization/architecture_diagrams.py +353 -0
  146. morphml/visualization/architecture_plot.py +223 -0
  147. morphml/visualization/convergence_plot.py +174 -0
  148. morphml/visualization/crossover_viz.py +386 -0
  149. morphml/visualization/graph_viz.py +338 -0
  150. morphml/visualization/pareto_plot.py +149 -0
  151. morphml/visualization/plotly_dashboards.py +422 -0
  152. morphml/visualization/population.py +309 -0
  153. morphml/visualization/progress.py +260 -0
  154. morphml-1.0.0.dist-info/METADATA +434 -0
  155. morphml-1.0.0.dist-info/RECORD +158 -0
  156. morphml-1.0.0.dist-info/WHEEL +4 -0
  157. morphml-1.0.0.dist-info/entry_points.txt +3 -0
  158. morphml-1.0.0.dist-info/licenses/LICENSE +21 -0
morphml/api/client.py ADDED
@@ -0,0 +1,338 @@
1
+ """API client library for MorphML.
2
+
3
+ Provides a Python client for interacting with the MorphML REST API.
4
+
5
+ Example:
6
+ >>> from morphml.api.client import MorphMLClient
7
+ >>> client = MorphMLClient("http://localhost:8000")
8
+ >>> experiments = client.list_experiments()
9
+ >>> exp = client.create_experiment("my-experiment", search_space={...})
10
+ """
11
+
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ import requests
15
+ from requests.exceptions import RequestException
16
+
17
+ from morphml.logging_config import get_logger
18
+
19
+ logger = get_logger(__name__)
20
+
21
+
22
+ class MorphMLClient:
23
+ """
24
+ Python client for MorphML REST API.
25
+
26
+ Provides convenient methods for all API endpoints.
27
+
28
+ Attributes:
29
+ base_url: Base URL of the API
30
+ token: Optional authentication token
31
+ session: Requests session
32
+
33
+ Example:
34
+ >>> client = MorphMLClient("http://localhost:8000")
35
+ >>> client.login("user@example.com", "password")
36
+ >>> experiments = client.list_experiments()
37
+ """
38
+
39
+ def __init__(self, base_url: str = "http://localhost:8000", token: Optional[str] = None):
40
+ """
41
+ Initialize API client.
42
+
43
+ Args:
44
+ base_url: Base URL of the API
45
+ token: Optional authentication token
46
+ """
47
+ self.base_url = base_url.rstrip("/")
48
+ self.token = token
49
+ self.session = requests.Session()
50
+
51
+ if token:
52
+ self.session.headers.update({"Authorization": f"Bearer {token}"})
53
+
54
+ logger.info(f"Initialized MorphML client for {base_url}")
55
+
56
+ def _request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
57
+ """
58
+ Make HTTP request to API.
59
+
60
+ Args:
61
+ method: HTTP method
62
+ endpoint: API endpoint
63
+ **kwargs: Additional request arguments
64
+
65
+ Returns:
66
+ Response JSON
67
+
68
+ Raises:
69
+ RequestException: If request fails
70
+ """
71
+ url = f"{self.base_url}{endpoint}"
72
+
73
+ try:
74
+ response = self.session.request(method, url, **kwargs)
75
+ response.raise_for_status()
76
+ return response.json()
77
+
78
+ except RequestException as e:
79
+ logger.error(f"API request failed: {e}")
80
+ raise
81
+
82
+ def login(self, username: str, password: str) -> str:
83
+ """
84
+ Login and get authentication token.
85
+
86
+ Args:
87
+ username: Username
88
+ password: Password
89
+
90
+ Returns:
91
+ Access token
92
+
93
+ Example:
94
+ >>> token = client.login("user@example.com", "password")
95
+ """
96
+ response = self._request(
97
+ "POST", "/api/v1/auth/login", json={"username": username, "password": password}
98
+ )
99
+
100
+ self.token = response["access_token"]
101
+ self.session.headers.update({"Authorization": f"Bearer {self.token}"})
102
+
103
+ logger.info(f"Logged in as {username}")
104
+
105
+ return self.token
106
+
107
+ # Experiment endpoints
108
+
109
+ def create_experiment(
110
+ self,
111
+ name: str,
112
+ search_space: Dict[str, Any],
113
+ optimizer: str = "genetic",
114
+ budget: int = 500,
115
+ config: Optional[Dict[str, Any]] = None,
116
+ ) -> Dict[str, Any]:
117
+ """
118
+ Create a new experiment.
119
+
120
+ Args:
121
+ name: Experiment name
122
+ search_space: Search space configuration
123
+ optimizer: Optimizer type
124
+ budget: Evaluation budget
125
+ config: Optional additional configuration
126
+
127
+ Returns:
128
+ Created experiment
129
+
130
+ Example:
131
+ >>> exp = client.create_experiment(
132
+ ... "cifar10-search",
133
+ ... search_space={"layers": [...]},
134
+ ... optimizer="genetic"
135
+ ... )
136
+ """
137
+ return self._request(
138
+ "POST",
139
+ "/api/v1/experiments",
140
+ json={
141
+ "name": name,
142
+ "search_space": search_space,
143
+ "optimizer": optimizer,
144
+ "budget": budget,
145
+ "config": config or {},
146
+ },
147
+ )
148
+
149
+ def list_experiments(
150
+ self, status: Optional[str] = None, limit: int = 100, offset: int = 0
151
+ ) -> List[Dict[str, Any]]:
152
+ """
153
+ List experiments.
154
+
155
+ Args:
156
+ status: Optional status filter
157
+ limit: Maximum results
158
+ offset: Offset for pagination
159
+
160
+ Returns:
161
+ List of experiments
162
+
163
+ Example:
164
+ >>> experiments = client.list_experiments(status="running")
165
+ """
166
+ params = {"limit": limit, "offset": offset}
167
+ if status:
168
+ params["status"] = status
169
+
170
+ return self._request("GET", "/api/v1/experiments", params=params)
171
+
172
+ def get_experiment(self, experiment_id: str) -> Dict[str, Any]:
173
+ """
174
+ Get experiment details.
175
+
176
+ Args:
177
+ experiment_id: Experiment ID
178
+
179
+ Returns:
180
+ Experiment details
181
+
182
+ Example:
183
+ >>> exp = client.get_experiment("exp_abc123")
184
+ """
185
+ return self._request("GET", f"/api/v1/experiments/{experiment_id}")
186
+
187
+ def start_experiment(self, experiment_id: str) -> Dict[str, Any]:
188
+ """
189
+ Start an experiment.
190
+
191
+ Args:
192
+ experiment_id: Experiment ID
193
+
194
+ Returns:
195
+ Updated experiment
196
+
197
+ Example:
198
+ >>> client.start_experiment("exp_abc123")
199
+ """
200
+ return self._request("POST", f"/api/v1/experiments/{experiment_id}/start")
201
+
202
+ def stop_experiment(self, experiment_id: str) -> Dict[str, Any]:
203
+ """
204
+ Stop a running experiment.
205
+
206
+ Args:
207
+ experiment_id: Experiment ID
208
+
209
+ Returns:
210
+ Updated experiment
211
+
212
+ Example:
213
+ >>> client.stop_experiment("exp_abc123")
214
+ """
215
+ return self._request("POST", f"/api/v1/experiments/{experiment_id}/stop")
216
+
217
+ def delete_experiment(self, experiment_id: str) -> None:
218
+ """
219
+ Delete an experiment.
220
+
221
+ Args:
222
+ experiment_id: Experiment ID
223
+
224
+ Example:
225
+ >>> client.delete_experiment("exp_abc123")
226
+ """
227
+ self._request("DELETE", f"/api/v1/experiments/{experiment_id}")
228
+ logger.info(f"Deleted experiment: {experiment_id}")
229
+
230
+ # Architecture endpoints
231
+
232
+ def list_architectures(
233
+ self,
234
+ experiment_id: Optional[str] = None,
235
+ min_fitness: Optional[float] = None,
236
+ limit: int = 100,
237
+ offset: int = 0,
238
+ ) -> List[Dict[str, Any]]:
239
+ """
240
+ List architectures.
241
+
242
+ Args:
243
+ experiment_id: Optional experiment filter
244
+ min_fitness: Optional minimum fitness filter
245
+ limit: Maximum results
246
+ offset: Offset for pagination
247
+
248
+ Returns:
249
+ List of architectures
250
+
251
+ Example:
252
+ >>> archs = client.list_architectures(
253
+ ... experiment_id="exp_abc123",
254
+ ... min_fitness=0.9
255
+ ... )
256
+ """
257
+ params = {"limit": limit, "offset": offset}
258
+ if experiment_id:
259
+ params["experiment_id"] = experiment_id
260
+ if min_fitness is not None:
261
+ params["min_fitness"] = min_fitness
262
+
263
+ return self._request("GET", "/api/v1/architectures", params=params)
264
+
265
+ def get_architecture(self, architecture_id: str) -> Dict[str, Any]:
266
+ """
267
+ Get architecture details.
268
+
269
+ Args:
270
+ architecture_id: Architecture ID
271
+
272
+ Returns:
273
+ Architecture details
274
+
275
+ Example:
276
+ >>> arch = client.get_architecture("arch_xyz789")
277
+ """
278
+ return self._request("GET", f"/api/v1/architectures/{architecture_id}")
279
+
280
+ # Utility methods
281
+
282
+ def health_check(self) -> Dict[str, Any]:
283
+ """
284
+ Check API health.
285
+
286
+ Returns:
287
+ Health status
288
+
289
+ Example:
290
+ >>> health = client.health_check()
291
+ >>> print(health["status"])
292
+ """
293
+ return self._request("GET", "/health")
294
+
295
+ def get_optimizers(self) -> List[Dict[str, Any]]:
296
+ """
297
+ Get list of available optimizers.
298
+
299
+ Returns:
300
+ List of optimizer information
301
+
302
+ Example:
303
+ >>> optimizers = client.get_optimizers()
304
+ >>> for opt in optimizers:
305
+ ... print(opt["name"])
306
+ """
307
+ return self._request("GET", "/api/v1/optimizers")
308
+
309
+
310
+ def create_client(
311
+ base_url: str = "http://localhost:8000",
312
+ username: Optional[str] = None,
313
+ password: Optional[str] = None,
314
+ ) -> MorphMLClient:
315
+ """
316
+ Create and optionally authenticate API client.
317
+
318
+ Args:
319
+ base_url: Base URL of the API
320
+ username: Optional username for authentication
321
+ password: Optional password for authentication
322
+
323
+ Returns:
324
+ MorphMLClient instance
325
+
326
+ Example:
327
+ >>> client = create_client(
328
+ ... "http://localhost:8000",
329
+ ... "user@example.com",
330
+ ... "password"
331
+ ... )
332
+ """
333
+ client = MorphMLClient(base_url)
334
+
335
+ if username and password:
336
+ client.login(username, password)
337
+
338
+ return client
morphml/api/models.py ADDED
@@ -0,0 +1,132 @@
1
+ """Pydantic models for API requests and responses."""
2
+
3
+ from datetime import datetime
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ class ExperimentCreate(BaseModel):
10
+ """Request model for creating an experiment."""
11
+
12
+ name: str = Field(..., description="Experiment name")
13
+ search_space_config: Dict[str, Any] = Field(..., description="Search space configuration")
14
+ optimizer_type: str = Field("genetic", description="Optimizer type")
15
+ optimizer_config: Dict[str, Any] = Field(
16
+ default_factory=dict, description="Optimizer configuration"
17
+ )
18
+ budget: int = Field(100, description="Evaluation budget")
19
+ constraints: Optional[List[Dict[str, Any]]] = Field(None, description="Constraints")
20
+
21
+ class Config:
22
+ schema_extra = {
23
+ "example": {
24
+ "name": "cifar10-search",
25
+ "search_space_config": {
26
+ "layers": [
27
+ {"type": "input", "shape": [3, 32, 32]},
28
+ {"type": "conv2d", "filters": [32, 64], "kernel_size": 3},
29
+ {"type": "flatten"},
30
+ {"type": "dense", "units": [128, 256]},
31
+ ]
32
+ },
33
+ "optimizer_type": "genetic",
34
+ "optimizer_config": {"population_size": 20, "num_generations": 50},
35
+ "budget": 1000,
36
+ }
37
+ }
38
+
39
+
40
+ class ExperimentResponse(BaseModel):
41
+ """Response model for experiment."""
42
+
43
+ id: str
44
+ name: str
45
+ status: str # pending, running, completed, failed
46
+ created_at: datetime
47
+ started_at: Optional[datetime] = None
48
+ completed_at: Optional[datetime] = None
49
+ best_fitness: Optional[float] = None
50
+ generations_completed: int = 0
51
+ total_generations: int
52
+
53
+ class Config:
54
+ schema_extra = {
55
+ "example": {
56
+ "id": "exp_123abc",
57
+ "name": "cifar10-search",
58
+ "status": "running",
59
+ "created_at": "2024-11-11T05:00:00Z",
60
+ "started_at": "2024-11-11T05:01:00Z",
61
+ "best_fitness": 0.9523,
62
+ "generations_completed": 25,
63
+ "total_generations": 50,
64
+ }
65
+ }
66
+
67
+
68
+ class ArchitectureResponse(BaseModel):
69
+ """Response model for architecture."""
70
+
71
+ id: str
72
+ experiment_id: str
73
+ fitness: float
74
+ parameters: int
75
+ depth: int
76
+ nodes: int
77
+ edges: int
78
+ created_at: datetime
79
+ graph_data: Optional[Dict[str, Any]] = None
80
+
81
+ class Config:
82
+ schema_extra = {
83
+ "example": {
84
+ "id": "arch_456def",
85
+ "experiment_id": "exp_123abc",
86
+ "fitness": 0.9523,
87
+ "parameters": 1234567,
88
+ "depth": 8,
89
+ "nodes": 12,
90
+ "edges": 11,
91
+ "created_at": "2024-11-11T05:15:00Z",
92
+ }
93
+ }
94
+
95
+
96
+ class SearchSpaceCreate(BaseModel):
97
+ """Request model for creating a search space."""
98
+
99
+ name: str
100
+ layers: List[Dict[str, Any]]
101
+ constraints: Optional[List[Dict[str, Any]]] = None
102
+
103
+
104
+ class OptimizerInfo(BaseModel):
105
+ """Information about an optimizer."""
106
+
107
+ name: str
108
+ type: str
109
+ description: str
110
+ parameters: Dict[str, Any]
111
+
112
+ class Config:
113
+ schema_extra = {
114
+ "example": {
115
+ "name": "GeneticAlgorithm",
116
+ "type": "evolutionary",
117
+ "description": "Genetic algorithm for neural architecture search",
118
+ "parameters": {
119
+ "population_size": {"type": "int", "default": 20},
120
+ "num_generations": {"type": "int", "default": 50},
121
+ "mutation_rate": {"type": "float", "default": 0.2},
122
+ },
123
+ }
124
+ }
125
+
126
+
127
+ class HealthResponse(BaseModel):
128
+ """Health check response."""
129
+
130
+ status: str
131
+ version: str
132
+ timestamp: datetime
@@ -0,0 +1,192 @@
1
+ """Rate limiting middleware for MorphML API.
2
+
3
+ Prevents API abuse by limiting request rates per client.
4
+
5
+ Example:
6
+ >>> from morphml.api.rate_limit import RateLimitMiddleware
7
+ >>> app.add_middleware(RateLimitMiddleware, requests_per_minute=60)
8
+ """
9
+
10
+ import time
11
+ from datetime import datetime, timedelta
12
+ from typing import Dict, Tuple
13
+
14
+ try:
15
+ from fastapi import HTTPException, Request, status
16
+ from starlette.middleware.base import BaseHTTPMiddleware
17
+
18
+ FASTAPI_AVAILABLE = True
19
+ except ImportError:
20
+ FASTAPI_AVAILABLE = False
21
+ Request = None
22
+ HTTPException = None
23
+ BaseHTTPMiddleware = None
24
+
25
+ from morphml.logging_config import get_logger
26
+
27
+ logger = get_logger(__name__)
28
+
29
+
30
+ class RateLimiter:
31
+ """
32
+ Simple in-memory rate limiter.
33
+
34
+ Tracks request counts per client IP address.
35
+
36
+ Attributes:
37
+ requests_per_minute: Maximum requests per minute
38
+ requests: Dictionary tracking requests per IP
39
+ """
40
+
41
+ def __init__(self, requests_per_minute: int = 60):
42
+ """
43
+ Initialize rate limiter.
44
+
45
+ Args:
46
+ requests_per_minute: Maximum requests per minute per IP
47
+ """
48
+ self.requests_per_minute = requests_per_minute
49
+ self.requests: Dict[str, list] = {}
50
+ self.cleanup_interval = 60 # Cleanup old entries every 60 seconds
51
+ self.last_cleanup = time.time()
52
+
53
+ def is_allowed(self, client_ip: str) -> Tuple[bool, int]:
54
+ """
55
+ Check if request is allowed for client.
56
+
57
+ Args:
58
+ client_ip: Client IP address
59
+
60
+ Returns:
61
+ Tuple of (is_allowed, remaining_requests)
62
+ """
63
+ now = datetime.now()
64
+ minute_ago = now - timedelta(minutes=1)
65
+
66
+ # Cleanup old entries periodically
67
+ if time.time() - self.last_cleanup > self.cleanup_interval:
68
+ self._cleanup()
69
+
70
+ # Get or create request list for this IP
71
+ if client_ip not in self.requests:
72
+ self.requests[client_ip] = []
73
+
74
+ # Remove requests older than 1 minute
75
+ self.requests[client_ip] = [
76
+ req_time for req_time in self.requests[client_ip] if req_time > minute_ago
77
+ ]
78
+
79
+ # Check if limit exceeded
80
+ request_count = len(self.requests[client_ip])
81
+
82
+ if request_count >= self.requests_per_minute:
83
+ return False, 0
84
+
85
+ # Add current request
86
+ self.requests[client_ip].append(now)
87
+
88
+ remaining = self.requests_per_minute - request_count - 1
89
+
90
+ return True, remaining
91
+
92
+ def _cleanup(self):
93
+ """Remove old entries to prevent memory leak."""
94
+ now = datetime.now()
95
+ minute_ago = now - timedelta(minutes=1)
96
+
97
+ # Remove IPs with no recent requests
98
+ ips_to_remove = []
99
+ for ip, req_times in self.requests.items():
100
+ recent_requests = [t for t in req_times if t > minute_ago]
101
+ if not recent_requests:
102
+ ips_to_remove.append(ip)
103
+ else:
104
+ self.requests[ip] = recent_requests
105
+
106
+ for ip in ips_to_remove:
107
+ del self.requests[ip]
108
+
109
+ self.last_cleanup = time.time()
110
+
111
+ if ips_to_remove:
112
+ logger.debug(f"Cleaned up {len(ips_to_remove)} inactive IPs")
113
+
114
+
115
+ class RateLimitMiddleware(BaseHTTPMiddleware if FASTAPI_AVAILABLE else object):
116
+ """
117
+ FastAPI middleware for rate limiting.
118
+
119
+ Automatically limits requests per client IP.
120
+
121
+ Example:
122
+ >>> app.add_middleware(
123
+ ... RateLimitMiddleware,
124
+ ... requests_per_minute=60
125
+ ... )
126
+ """
127
+
128
+ def __init__(self, app, requests_per_minute: int = 60):
129
+ """
130
+ Initialize middleware.
131
+
132
+ Args:
133
+ app: FastAPI application
134
+ requests_per_minute: Maximum requests per minute
135
+ """
136
+ if not FASTAPI_AVAILABLE:
137
+ raise ImportError("FastAPI required for rate limiting")
138
+
139
+ super().__init__(app)
140
+ self.limiter = RateLimiter(requests_per_minute)
141
+ logger.info(f"Rate limiting enabled: {requests_per_minute} requests/minute")
142
+
143
+ async def dispatch(self, request: Request, call_next):
144
+ """
145
+ Process request with rate limiting.
146
+
147
+ Args:
148
+ request: Incoming request
149
+ call_next: Next middleware/handler
150
+
151
+ Returns:
152
+ Response
153
+ """
154
+ # Get client IP
155
+ client_ip = request.client.host
156
+
157
+ # Check rate limit
158
+ allowed, remaining = self.limiter.is_allowed(client_ip)
159
+
160
+ if not allowed:
161
+ logger.warning(f"Rate limit exceeded for {client_ip}")
162
+ raise HTTPException(
163
+ status_code=status.HTTP_429_TOO_MANY_REQUESTS,
164
+ detail="Rate limit exceeded. Please try again later.",
165
+ headers={"Retry-After": "60"},
166
+ )
167
+
168
+ # Process request
169
+ response = await call_next(request)
170
+
171
+ # Add rate limit headers
172
+ response.headers["X-RateLimit-Limit"] = str(self.limiter.requests_per_minute)
173
+ response.headers["X-RateLimit-Remaining"] = str(remaining)
174
+
175
+ return response
176
+
177
+
178
+ def create_rate_limiter(requests_per_minute: int = 60) -> RateLimiter:
179
+ """
180
+ Create a rate limiter instance.
181
+
182
+ Args:
183
+ requests_per_minute: Maximum requests per minute
184
+
185
+ Returns:
186
+ RateLimiter instance
187
+
188
+ Example:
189
+ >>> limiter = create_rate_limiter(100)
190
+ >>> allowed, remaining = limiter.is_allowed("192.168.1.1")
191
+ """
192
+ return RateLimiter(requests_per_minute)
@@ -0,0 +1,36 @@
1
+ """Benchmarking and comparison tools for optimizer evaluation.
2
+
3
+ This module provides tools to systematically compare and evaluate
4
+ different optimization algorithms.
5
+
6
+ Features:
7
+ - Optimizer comparison with statistical analysis
8
+ - Convergence visualization
9
+ - Sample efficiency analysis
10
+ - Result reporting
11
+
12
+ Example:
13
+ >>> from morphml.benchmarking import OptimizerComparison, compare_optimizers
14
+ >>> from morphml.optimizers import GeneticAlgorithm, optimize_with_pso
15
+ >>>
16
+ >>> # Quick comparison
17
+ >>> results = compare_optimizers(
18
+ ... optimizers={
19
+ ... 'GA': GeneticAlgorithm(space, config),
20
+ ... 'PSO': ParticleSwarmOptimizer(space, config)
21
+ ... },
22
+ ... search_space=space,
23
+ ... evaluator=my_evaluator,
24
+ ... budget=100
25
+ ... )
26
+ """
27
+
28
+ from morphml.benchmarking.comparison import (
29
+ OptimizerComparison,
30
+ compare_optimizers,
31
+ )
32
+
33
+ __all__ = [
34
+ "OptimizerComparison",
35
+ "compare_optimizers",
36
+ ]