microlens-submit 0.12.2__py3-none-any.whl → 0.16.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.
Files changed (34) hide show
  1. microlens_submit/__init__.py +7 -157
  2. microlens_submit/cli/__init__.py +5 -0
  3. microlens_submit/cli/__main__.py +6 -0
  4. microlens_submit/cli/commands/__init__.py +1 -0
  5. microlens_submit/cli/commands/dossier.py +139 -0
  6. microlens_submit/cli/commands/export.py +177 -0
  7. microlens_submit/cli/commands/init.py +172 -0
  8. microlens_submit/cli/commands/solutions.py +722 -0
  9. microlens_submit/cli/commands/validation.py +241 -0
  10. microlens_submit/cli/main.py +120 -0
  11. microlens_submit/dossier/__init__.py +51 -0
  12. microlens_submit/dossier/dashboard.py +503 -0
  13. microlens_submit/dossier/event_page.py +370 -0
  14. microlens_submit/dossier/full_report.py +330 -0
  15. microlens_submit/dossier/solution_page.py +534 -0
  16. microlens_submit/dossier/utils.py +111 -0
  17. microlens_submit/error_messages.py +283 -0
  18. microlens_submit/models/__init__.py +28 -0
  19. microlens_submit/models/event.py +406 -0
  20. microlens_submit/models/solution.py +569 -0
  21. microlens_submit/models/submission.py +569 -0
  22. microlens_submit/tier_validation.py +208 -0
  23. microlens_submit/utils.py +373 -0
  24. microlens_submit/validate_parameters.py +478 -180
  25. {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.1.dist-info}/METADATA +52 -14
  26. microlens_submit-0.16.1.dist-info/RECORD +32 -0
  27. microlens_submit/api.py +0 -1257
  28. microlens_submit/cli.py +0 -1803
  29. microlens_submit/dossier.py +0 -1443
  30. microlens_submit-0.12.2.dist-info/RECORD +0 -13
  31. {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.1.dist-info}/WHEEL +0 -0
  32. {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.1.dist-info}/entry_points.txt +0 -0
  33. {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.1.dist-info}/licenses/LICENSE +0 -0
  34. {microlens_submit-0.12.2.dist-info → microlens_submit-0.16.1.dist-info}/top_level.txt +0 -0
@@ -31,7 +31,7 @@ The module defines:
31
31
 
32
32
  Example:
33
33
  >>> from microlens_submit.validate_parameters import check_solution_completeness
34
- >>>
34
+ >>>
35
35
  >>> # Validate a simple 1S1L solution
36
36
  >>> parameters = {"t0": 2459123.5, "u0": 0.1, "tE": 20.0}
37
37
  >>> messages = check_solution_completeness("1S1L", parameters)
@@ -39,7 +39,7 @@ Example:
39
39
  ... print("Solution is complete!")
40
40
  >>> else:
41
41
  ... print("Issues found:", messages)
42
-
42
+
43
43
  >>> # Validate a binary lens with parallax
44
44
  >>> parameters = {
45
45
  ... "t0": 2459123.5, "u0": 0.1, "tE": 20.0,
@@ -57,8 +57,7 @@ Note:
57
57
  custom parameters and future model types.
58
58
  """
59
59
 
60
- from typing import Dict, List, Any, Optional, Union
61
-
60
+ from typing import Any, Dict, List, Optional
62
61
 
63
62
  MODEL_DEFINITIONS = {
64
63
  # Single Source, Single Lens (PSPL)
@@ -74,19 +73,36 @@ MODEL_DEFINITIONS = {
74
73
  # Binary Source, Single Lens
75
74
  "2S1L": {
76
75
  "description": "Binary Source, Single Point Lens",
77
- "required_params_core": ["t0", "u0", "tE"], # Core lens params
76
+ "required_params_core": ["t0", "u0", "tE"], # Core lens params
77
+ },
78
+ # Other/Unknown model type (allows any parameters)
79
+ "other": {
80
+ "description": "Other or unknown model type",
81
+ "required_params_core": [], # No required parameters for unknown models
78
82
  },
79
83
  # Add other model types as needed:
80
- # "2S2L": { "description": "Binary Source, Binary Point Lens", "required_params_core": ["t0", "u0", "tE", "s", "q", "alpha"]},
81
- # "1S3L": { "description": "Point Source, Triple Point Lens", "required_params_core": ["t0", "u0", "tE", "s1", "q1", "alpha1", "s2", "q2", "alpha2"]},
82
- # "2S3L": { "description": "Binary Source, Triple Point Lens", "required_params_core": ["t0", "u0", "tE", "s1", "q1", "alpha1", "s2", "q2", "alpha2"]},
84
+ # "2S2L": {
85
+ # "description": "Binary Source, Binary Point Lens",
86
+ # "required_params_core": ["t0", "u0", "tE", "s", "q", "alpha"]
87
+ # },
88
+ # "1S3L": {
89
+ # "description": "Point Source, Triple Point Lens",
90
+ # "required_params_core": ["t0", "u0", "tE", "s1", "q1", "alpha1", "s2", "q2", "alpha2"]
91
+ # },
92
+ # "2S3L": {
93
+ # "description": "Binary Source, Triple Point Lens",
94
+ # "required_params_core": ["t0", "u0", "tE", "s1", "q1", "alpha1", "s2", "q2", "alpha2"]
95
+ # },
83
96
  }
84
97
 
85
98
  HIGHER_ORDER_EFFECT_DEFINITIONS = {
86
99
  "parallax": {
87
100
  "description": "Microlens parallax effect",
88
- "requires_t_ref": True, # A flag to check for the 't_ref' attribute
89
- "required_higher_order_params": ["piEN", "piEE"], # These are often part of the main parameters if fitted
101
+ "requires_t_ref": True, # A flag to check for the 't_ref' attribute
102
+ "required_higher_order_params": [
103
+ "piEN",
104
+ "piEE",
105
+ ], # These are often part of the main parameters if fitted
90
106
  },
91
107
  "finite-source": {
92
108
  "description": "Finite source size effect",
@@ -97,7 +113,7 @@ HIGHER_ORDER_EFFECT_DEFINITIONS = {
97
113
  "description": "Orbital motion of the lens components",
98
114
  "requires_t_ref": True,
99
115
  "required_higher_order_params": ["dsdt", "dadt"],
100
- "optional_higher_order_params": ["dzdt"], # Relative radial rate of change of lenses (if needed)
116
+ "optional_higher_order_params": ["dzdt"], # Relative radial rate of change of lenses (if needed)
101
117
  },
102
118
  "xallarap": {
103
119
  "description": "Source orbital motion (xallarap)",
@@ -106,104 +122,246 @@ HIGHER_ORDER_EFFECT_DEFINITIONS = {
106
122
  },
107
123
  "gaussian-process": {
108
124
  "description": "Gaussian process model for time-correlated noise",
109
- "requires_t_ref": False, # GP parameters are usually not time-referenced in this way
110
- "required_higher_order_params": [], # Placeholder for common GP hyperparameters
111
- "optional_higher_order_params": ["ln_K", "ln_lambda", "ln_period", "ln_gamma"], # Common GP params, or specific names like "amplitude", "timescale", "periodicity" etc.
125
+ "requires_t_ref": False, # GP parameters are usually not time-referenced in this way
126
+ "required_higher_order_params": [], # Placeholder for common GP hyperparameters
127
+ "optional_higher_order_params": [
128
+ "ln_K",
129
+ "ln_lambda",
130
+ "ln_period",
131
+ "ln_gamma",
132
+ ], # Common GP params, or specific names like "amplitude", "timescale", "periodicity" etc.
112
133
  },
113
134
  "stellar-rotation": {
114
135
  "description": "Effect of stellar rotation on the light curve (e.g., spots)",
115
- "requires_t_ref": False, # Usually not time-referenced directly in this context
116
- "required_higher_order_params": [], # Specific parameters (e.g., rotation period, inclination) to be added here
117
- "optional_higher_order_params": ["v_rot_sin_i", "epsilon"], # Guessing common params: rotational velocity times sin(inclination), spot coverage
136
+ "requires_t_ref": False, # Usually not time-referenced directly in this context
137
+ "required_higher_order_params": [], # Specific parameters
138
+ # (e.g., rotation period, inclination)
139
+ # to be added here
140
+ "optional_higher_order_params": [
141
+ "v_rot_sin_i",
142
+ "epsilon",
143
+ ], # Guessing common params: rotational velocity times sin(inclination),
144
+ # spot coverage
118
145
  },
119
146
  "fitted-limb-darkening": {
120
147
  "description": "Limb darkening coefficients fitted as parameters",
121
148
  "requires_t_ref": False,
122
- "required_higher_order_params": [], # Parameters are usually u1, u2, etc. (linear, quadratic)
123
- "optional_higher_order_params": ["u1", "u2", "u3", "u4"], # Common limb darkening coefficients (linear, quadratic, cubic, quartic)
149
+ "required_higher_order_params": [], # Parameters are usually u1, u2, etc.
150
+ # (linear, quadratic)
151
+ "optional_higher_order_params": [
152
+ "u1",
153
+ "u2",
154
+ "u3",
155
+ "u4",
156
+ ], # Common limb darkening coefficients (linear, quadratic, cubic, quartic)
124
157
  },
125
- # The "other" effect type is handled by allowing any other string in `higher_order_effects` list itself.
158
+ # The "other" effect type is handled by allowing any other string in
159
+ # `higher_order_effects` list itself.
126
160
  }
127
161
 
128
162
  # This dictionary defines properties/constraints for each known parameter
129
- # (e.g., expected type, units, a more detailed description, corresponding uncertainty field name)
163
+ # (e.g., expected type, units, a more detailed description, corresponding
164
+ # uncertainty field name)
130
165
  PARAMETER_PROPERTIES = {
131
166
  # Core Microlensing Parameters
132
167
  "t0": {"type": "float", "units": "HJD", "description": "Time of closest approach"},
133
- "u0": {"type": "float", "units": "thetaE", "description": "Minimum impact parameter"},
134
- "tE": {"type": "float", "units": "days", "description": "Einstein radius crossing time"},
135
- "s": {"type": "float", "units": "thetaE", "description": "Binary separation scaled by Einstein radius"},
168
+ "u0": {
169
+ "type": "float",
170
+ "units": "thetaE",
171
+ "description": "Minimum impact parameter",
172
+ },
173
+ "tE": {
174
+ "type": "float",
175
+ "units": "days",
176
+ "description": "Einstein radius crossing time",
177
+ },
178
+ "s": {
179
+ "type": "float",
180
+ "units": "thetaE",
181
+ "description": "Binary separation scaled by Einstein radius",
182
+ },
136
183
  "q": {"type": "float", "units": "mass ratio", "description": "Mass ratio M2/M1"},
137
- "alpha": {"type": "float", "units": "rad", "description": "Angle of source trajectory relative to binary axis"},
138
-
184
+ "alpha": {
185
+ "type": "float",
186
+ "units": "rad",
187
+ "description": "Angle of source trajectory relative to binary axis",
188
+ },
139
189
  # Higher-Order Effect Parameters
140
- "rho": {"type": "float", "units": "thetaE", "description": "Source radius scaled by Einstein radius (Finite Source)"},
141
- "piEN": {"type": "float", "units": "Einstein radius", "description": "Parallax vector component (North) (Parallax)"},
142
- "piEE": {"type": "float", "units": "Einstein radius", "description": "Parallax vector component (East) (Parallax)"},
143
- "dsdt": {"type": "float", "units": "thetaE/year", "description": "Rate of change of binary separation (Lens Orbital Motion)"},
144
- "dadt": {"type": "float", "units": "rad/year", "description": "Rate of change of binary angle (Lens Orbital Motion)"},
145
- "dzdt": {"type": "float", "units": "au/year", "description": "Relative radial rate of change of lenses (Lens Orbital Motion, if applicable)"}, # Example, may vary
146
-
190
+ "rho": {
191
+ "type": "float",
192
+ "units": "thetaE",
193
+ "description": "Source radius scaled by Einstein radius (Finite Source)",
194
+ },
195
+ "piEN": {
196
+ "type": "float",
197
+ "units": "Einstein radius",
198
+ "description": "Parallax vector component (North) (Parallax)",
199
+ },
200
+ "piEE": {
201
+ "type": "float",
202
+ "units": "Einstein radius",
203
+ "description": "Parallax vector component (East) (Parallax)",
204
+ },
205
+ "dsdt": {
206
+ "type": "float",
207
+ "units": "thetaE/year",
208
+ "description": "Rate of change of binary separation (Lens Orbital Motion)",
209
+ },
210
+ "dadt": {
211
+ "type": "float",
212
+ "units": "rad/year",
213
+ "description": "Rate of change of binary angle (Lens Orbital Motion)",
214
+ },
215
+ "dzdt": {
216
+ "type": "float",
217
+ "units": "au/year",
218
+ "description": "Relative radial rate of change of lenses (Lens Orbital Motion, if applicable)",
219
+ }, # Example, may vary
147
220
  # Flux Parameters (dynamically generated by get_required_flux_params)
148
221
  # Ensure these names precisely match how they're generated by get_required_flux_params
149
- "F0_S": {"type": "float", "units": "counts/s", "description": "Source flux in band 0"},
150
- "F0_B": {"type": "float", "units": "counts/s", "description": "Blend flux in band 0"},
151
- "F1_S": {"type": "float", "units": "counts/s", "description": "Source flux in band 1"},
152
- "F1_B": {"type": "float", "units": "counts/s", "description": "Blend flux in band 1"},
153
- "F2_S": {"type": "float", "units": "counts/s", "description": "Source flux in band 2"},
154
- "F2_B": {"type": "float", "units": "counts/s", "description": "Blend flux in band 2"},
155
-
222
+ "F0_S": {
223
+ "type": "float",
224
+ "units": "counts/s",
225
+ "description": "Source flux in band 0",
226
+ },
227
+ "F0_B": {
228
+ "type": "float",
229
+ "units": "counts/s",
230
+ "description": "Blend flux in band 0",
231
+ },
232
+ "F1_S": {
233
+ "type": "float",
234
+ "units": "counts/s",
235
+ "description": "Source flux in band 1",
236
+ },
237
+ "F1_B": {
238
+ "type": "float",
239
+ "units": "counts/s",
240
+ "description": "Blend flux in band 1",
241
+ },
242
+ "F2_S": {
243
+ "type": "float",
244
+ "units": "counts/s",
245
+ "description": "Source flux in band 2",
246
+ },
247
+ "F2_B": {
248
+ "type": "float",
249
+ "units": "counts/s",
250
+ "description": "Blend flux in band 2",
251
+ },
156
252
  # Binary Source Flux Parameters (e.g., for "2S" models)
157
- "F0_S1": {"type": "float", "units": "counts/s", "description": "Primary source flux in band 0"},
158
- "F0_S2": {"type": "float", "units": "counts/s", "description": "Secondary source flux in band 0"},
159
- "F1_S1": {"type": "float", "units": "counts/s", "description": "Primary source flux in band 1"},
160
- "F1_S2": {"type": "float", "units": "counts/s", "description": "Secondary source flux in band 1"},
161
- "F2_S1": {"type": "float", "units": "counts/s", "description": "Primary source flux in band 2"},
162
- "F2_S2": {"type": "float", "units": "counts/s", "description": "Secondary source flux in band 2"},
163
-
253
+ "F0_S1": {
254
+ "type": "float",
255
+ "units": "counts/s",
256
+ "description": "Primary source flux in band 0",
257
+ },
258
+ "F0_S2": {
259
+ "type": "float",
260
+ "units": "counts/s",
261
+ "description": "Secondary source flux in band 0",
262
+ },
263
+ "F1_S1": {
264
+ "type": "float",
265
+ "units": "counts/s",
266
+ "description": "Primary source flux in band 1",
267
+ },
268
+ "F1_S2": {
269
+ "type": "float",
270
+ "units": "counts/s",
271
+ "description": "Secondary source flux in band 1",
272
+ },
273
+ "F2_S1": {
274
+ "type": "float",
275
+ "units": "counts/s",
276
+ "description": "Primary source flux in band 2",
277
+ },
278
+ "F2_S2": {
279
+ "type": "float",
280
+ "units": "counts/s",
281
+ "description": "Secondary source flux in band 2",
282
+ },
164
283
  # Gaussian Process parameters (examples, often ln-scaled)
165
- "ln_K": {"type": "float", "units": "mag^2", "description": "Log-amplitude of the GP kernel (GP)"},
166
- "ln_lambda": {"type": "float", "units": "days", "description": "Log-lengthscale of the GP kernel (GP)"},
167
- "ln_period": {"type": "float", "units": "days", "description": "Log-period of the GP kernel (GP)"},
168
- "ln_gamma": {"type": "float", "units": " ", "description": "Log-smoothing parameter of the GP kernel (GP)"}, # Specific interpretation varies by kernel
169
-
284
+ "ln_K": {
285
+ "type": "float",
286
+ "units": "mag^2",
287
+ "description": "Log-amplitude of the GP kernel (GP)",
288
+ },
289
+ "ln_lambda": {
290
+ "type": "float",
291
+ "units": "days",
292
+ "description": "Log-lengthscale of the GP kernel (GP)",
293
+ },
294
+ "ln_period": {
295
+ "type": "float",
296
+ "units": "days",
297
+ "description": "Log-period of the GP kernel (GP)",
298
+ },
299
+ "ln_gamma": {
300
+ "type": "float",
301
+ "units": " ",
302
+ "description": "Log-smoothing parameter of the GP kernel (GP)",
303
+ }, # Specific interpretation varies by kernel
170
304
  # Stellar Rotation parameters (examples)
171
- "v_rot_sin_i": {"type": "float", "units": "km/s", "description": "Rotational velocity times sin(inclination) (Stellar Rotation)"},
172
- "epsilon": {"type": "float", "units": " ", "description": "Spot coverage/brightness parameter (Stellar Rotation)"}, # Example, may vary
173
-
305
+ "v_rot_sin_i": {
306
+ "type": "float",
307
+ "units": "km/s",
308
+ "description": "Rotational velocity times sin(inclination) (Stellar Rotation)",
309
+ },
310
+ "epsilon": {
311
+ "type": "float",
312
+ "units": " ",
313
+ "description": "Spot coverage/brightness parameter (Stellar Rotation)",
314
+ }, # Example, may vary
174
315
  # Fitted Limb Darkening coefficients (examples)
175
- "u1": {"type": "float", "units": " ", "description": "Linear limb darkening coefficient (Fitted Limb Darkening)"},
176
- "u2": {"type": "float", "units": " ", "description": "Quadratic limb darkening coefficient (Fitted Limb Darkening)"},
177
- "u3": {"type": "float", "units": " ", "description": "Cubic limb darkening coefficient (Fitted Limb Darkening)"},
178
- "u4": {"type": "float", "units": " ", "description": "Quartic limb darkening coefficient (Fitted Limb Darkening)"},
316
+ "u1": {
317
+ "type": "float",
318
+ "units": " ",
319
+ "description": "Linear limb darkening coefficient (Fitted Limb Darkening)",
320
+ },
321
+ "u2": {
322
+ "type": "float",
323
+ "units": " ",
324
+ "description": "Quadratic limb darkening coefficient (Fitted Limb Darkening)",
325
+ },
326
+ "u3": {
327
+ "type": "float",
328
+ "units": " ",
329
+ "description": "Cubic limb darkening coefficient (Fitted Limb Darkening)",
330
+ },
331
+ "u4": {
332
+ "type": "float",
333
+ "units": " ",
334
+ "description": "Quartic limb darkening coefficient (Fitted Limb Darkening)",
335
+ },
179
336
  }
180
337
 
338
+
181
339
  def get_required_flux_params(model_type: str, bands: List[str]) -> List[str]:
182
340
  """Get the required flux parameters for a given model type and bands.
183
-
341
+
184
342
  Determines which flux parameters are required based on the model type
185
343
  (single vs binary source) and the photometric bands used. For single
186
344
  source models, each band requires source and blend flux parameters.
187
345
  For binary source models, each band requires two source fluxes and
188
346
  a common blend flux.
189
-
347
+
190
348
  Args:
191
349
  model_type: The type of microlensing model (e.g., "1S1L", "2S1L").
192
350
  bands: List of band IDs as strings (e.g., ["0", "1", "2"]).
193
-
351
+
194
352
  Returns:
195
353
  List of required flux parameter names (e.g., ["F0_S", "F0_B", "F1_S", "F1_B"]).
196
-
354
+
197
355
  Example:
198
356
  >>> get_required_flux_params("1S1L", ["0", "1"])
199
357
  ['F0_S', 'F0_B', 'F1_S', 'F1_B']
200
-
358
+
201
359
  >>> get_required_flux_params("2S1L", ["0", "1"])
202
360
  ['F0_S1', 'F0_S2', 'F0_B', 'F1_S1', 'F1_S2', 'F1_B']
203
-
361
+
204
362
  >>> get_required_flux_params("1S1L", [])
205
363
  []
206
-
364
+
207
365
  Note:
208
366
  This function handles the most common model types (1S and 2S).
209
367
  For models with more than 2 sources, additional logic would be needed.
@@ -211,16 +369,17 @@ def get_required_flux_params(model_type: str, bands: List[str]) -> List[str]:
211
369
  """
212
370
  flux_params = []
213
371
  if not bands:
214
- return flux_params # No bands, no flux parameters
215
-
372
+ return flux_params # No bands, no flux parameters
373
+
216
374
  for band in bands:
217
- if model_type.startswith("1S"): # Single source models
218
- flux_params.append(f"F{band}_S") # Source flux for this band
219
- flux_params.append(f"F{band}_B") # Blend flux for this band
220
- elif model_type.startswith("2S"): # Binary source models
221
- flux_params.append(f"F{band}_S1") # First source flux for this band
222
- flux_params.append(f"F{band}_S2") # Second source flux for this band
223
- flux_params.append(f"F{band}_B") # Blend flux for this band (common for binary sources)
375
+ if model_type.startswith("1S"): # Single source models
376
+ flux_params.append(f"F{band}_S") # Source flux for this band
377
+ flux_params.append(f"F{band}_B") # Blend flux for this band
378
+ elif model_type.startswith("2S"): # Binary source models
379
+ flux_params.append(f"F{band}_S1") # First source flux for this band
380
+ flux_params.append(f"F{band}_S2") # Second source flux for this band
381
+ flux_params.append(f"F{band}_B") # Blend flux for this band
382
+ # (common for binary sources)
224
383
  # Add more source types (e.g., 3S) if necessary in the future
225
384
  return flux_params
226
385
 
@@ -231,21 +390,21 @@ def check_solution_completeness(
231
390
  higher_order_effects: Optional[List[str]] = None,
232
391
  bands: Optional[List[str]] = None,
233
392
  t_ref: Optional[float] = None,
234
- **kwargs
393
+ **kwargs,
235
394
  ) -> List[str]:
236
395
  """Check if a solution has all required parameters based on its model type and effects.
237
-
396
+
238
397
  This function validates that all required parameters are present for the given
239
398
  model type and any higher-order effects. It returns a list of human-readable
240
399
  warning or error messages instead of raising exceptions immediately.
241
-
400
+
242
401
  The validation checks:
243
402
  - Required core parameters for the model type
244
403
  - Required parameters for each higher-order effect
245
404
  - Flux parameters for specified photometric bands
246
405
  - Reference time requirements for time-dependent effects
247
406
  - Recognition of unknown parameters (warnings only)
248
-
407
+
249
408
  Args:
250
409
  model_type: The type of microlensing model (e.g., '1S1L', '1S2L').
251
410
  parameters: Dictionary of model parameters with parameter names as keys.
@@ -256,40 +415,45 @@ def check_solution_completeness(
256
415
  t_ref: Reference time for time-dependent effects (Julian Date).
257
416
  Required for effects that specify requires_t_ref=True.
258
417
  **kwargs: Additional solution attributes to validate (currently unused).
259
-
418
+
260
419
  Returns:
261
420
  List of validation messages. Empty list if all validations pass.
262
421
  Messages indicate missing required parameters, unknown effects,
263
422
  missing reference times, or unrecognized parameters.
264
-
423
+
265
424
  Example:
266
425
  >>> # Simple 1S1L solution - should pass
267
426
  >>> params = {"t0": 2459123.5, "u0": 0.1, "tE": 20.0}
268
427
  >>> messages = check_solution_completeness("1S1L", params)
269
428
  >>> print(messages)
270
429
  []
271
-
430
+
272
431
  >>> # Missing required parameter
273
432
  >>> params = {"t0": 2459123.5, "u0": 0.1} # Missing tE
274
433
  >>> messages = check_solution_completeness("1S1L", params)
275
434
  >>> print(messages)
276
435
  ["Missing required core parameter 'tE' for model type '1S1L'"]
277
-
436
+
278
437
  >>> # Binary lens with parallax
279
438
  >>> params = {
280
439
  ... "t0": 2459123.5, "u0": 0.1, "tE": 20.0,
281
440
  ... "s": 1.2, "q": 0.5, "alpha": 45.0,
282
441
  ... "piEN": 0.1, "piEE": 0.05
283
442
  ... }
284
- >>> messages = check_solution_completeness("1S2L", params, ["parallax"], t_ref=2459123.0)
443
+ >>> messages = check_solution_completeness(
444
+ ... "1S2L",
445
+ ... params,
446
+ ... ["parallax"],
447
+ ... t_ref=2459123.0
448
+ ... )
285
449
  >>> print(messages)
286
450
  []
287
-
451
+
288
452
  >>> # Missing reference time for parallax
289
453
  >>> messages = check_solution_completeness("1S2L", params, ["parallax"])
290
454
  >>> print(messages)
291
455
  ["Reference time (t_ref) required for effect 'parallax'"]
292
-
456
+
293
457
  Note:
294
458
  This function is designed to be comprehensive but not overly strict.
295
459
  Unknown parameters generate warnings rather than errors to accommodate
@@ -297,116 +461,119 @@ def check_solution_completeness(
297
461
  the predefined MODEL_DEFINITIONS and HIGHER_ORDER_EFFECT_DEFINITIONS.
298
462
  """
299
463
  messages = []
300
-
464
+
301
465
  # Validate model type
302
466
  if model_type not in MODEL_DEFINITIONS:
303
- messages.append(f"Unknown model type: '{model_type}'. Valid types: {list(MODEL_DEFINITIONS.keys())}")
467
+ messages.append(f"Unknown model type: '{model_type}'. " f"Valid types: {list(MODEL_DEFINITIONS.keys())}")
304
468
  return messages
305
-
469
+
306
470
  model_def = MODEL_DEFINITIONS[model_type]
307
-
471
+
308
472
  # Check required core parameters
309
- required_core_params = model_def.get('required_params_core', [])
473
+ required_core_params = model_def.get("required_params_core", [])
310
474
  for param in required_core_params:
311
475
  if param not in parameters:
312
- messages.append(f"Missing required core parameter '{param}' for model type '{model_type}'")
313
-
476
+ messages.append(f"Missing required core parameter '{param}' for model type " f"'{model_type}'")
477
+
314
478
  # Validate higher-order effects
315
479
  if higher_order_effects:
316
480
  for effect in higher_order_effects:
317
481
  if effect not in HIGHER_ORDER_EFFECT_DEFINITIONS:
318
- messages.append(f"Unknown higher-order effect: '{effect}'. Valid effects: {list(HIGHER_ORDER_EFFECT_DEFINITIONS.keys())}")
482
+ messages.append(
483
+ f"Unknown higher-order effect: '{effect}'. "
484
+ f"Valid effects: {list(HIGHER_ORDER_EFFECT_DEFINITIONS.keys())}"
485
+ )
319
486
  continue
320
-
487
+
321
488
  effect_def = HIGHER_ORDER_EFFECT_DEFINITIONS[effect]
322
-
489
+
323
490
  # Check required parameters for this effect
324
- effect_required = effect_def.get('required_higher_order_params', [])
491
+ effect_required = effect_def.get("required_higher_order_params", [])
325
492
  for param in effect_required:
326
493
  if param not in parameters:
327
- messages.append(f"Missing required parameter '{param}' for effect '{effect}'")
328
-
494
+ messages.append(f"Missing required parameter '{param}' for effect " f"'{effect}'")
495
+
329
496
  # Check optional parameters for this effect
330
- effect_optional = effect_def.get('optional_higher_order_params', [])
497
+ effect_optional = effect_def.get("optional_higher_order_params", [])
331
498
  for param in effect_optional:
332
499
  if param not in parameters:
333
- messages.append(f"Warning: Optional parameter '{param}' not provided for effect '{effect}'")
334
-
500
+ messages.append(f"Warning: Optional parameter '{param}' not provided " f"for effect '{effect}'")
501
+
335
502
  # Check if t_ref is required for this effect
336
- if effect_def.get('requires_t_ref', False) and t_ref is None:
503
+ if effect_def.get("requires_t_ref", False) and t_ref is None:
337
504
  messages.append(f"Reference time (t_ref) required for effect '{effect}'")
338
-
505
+
339
506
  # Validate band-specific parameters
340
507
  if bands:
341
508
  required_flux_params = get_required_flux_params(model_type, bands)
342
509
  for param in required_flux_params:
343
510
  if param not in parameters:
344
- messages.append(f"Missing required flux parameter '{param}' for bands {bands}")
345
-
511
+ messages.append(f"Missing required flux parameter '{param}' for bands " f"{bands}")
512
+
346
513
  # Check for invalid parameters (not in any definition)
347
514
  all_valid_params = set()
348
-
515
+
349
516
  # Add core model parameters
350
517
  all_valid_params.update(required_core_params)
351
-
518
+
352
519
  # Add higher-order effect parameters
353
520
  if higher_order_effects:
354
521
  for effect in higher_order_effects:
355
522
  if effect in HIGHER_ORDER_EFFECT_DEFINITIONS:
356
523
  effect_def = HIGHER_ORDER_EFFECT_DEFINITIONS[effect]
357
- all_valid_params.update(effect_def.get('required_higher_order_params', []))
358
- all_valid_params.update(effect_def.get('optional_higher_order_params', []))
359
-
524
+ all_valid_params.update(effect_def.get("required_higher_order_params", []))
525
+ all_valid_params.update(effect_def.get("optional_higher_order_params", []))
526
+
360
527
  # Add band-specific parameters if bands are specified
361
528
  if bands:
362
529
  all_valid_params.update(get_required_flux_params(model_type, bands))
363
-
530
+
364
531
  # Check for invalid parameters
365
532
  invalid_params = set(parameters.keys()) - all_valid_params
366
533
  for param in invalid_params:
367
- messages.append(f"Warning: Parameter '{param}' not recognized for model type '{model_type}'")
368
-
534
+ messages.append(f"Warning: Parameter '{param}' not recognized for model type " f"'{model_type}'")
535
+
369
536
  return messages
370
537
 
371
538
 
372
539
  def validate_parameter_types(
373
540
  parameters: Dict[str, Any],
374
- model_type: str
541
+ model_type: str,
375
542
  ) -> List[str]:
376
543
  """Validate parameter types and value ranges against expected types.
377
-
544
+
378
545
  Checks that parameters have the correct data types as defined in
379
546
  PARAMETER_PROPERTIES. Currently supports validation of float, int,
380
547
  and string types. Parameters not defined in PARAMETER_PROPERTIES
381
548
  are skipped (no validation performed).
382
-
549
+
383
550
  Args:
384
551
  parameters: Dictionary of model parameters with parameter names as keys.
385
552
  model_type: The type of microlensing model (used for context in messages).
386
-
553
+
387
554
  Returns:
388
555
  List of validation messages. Empty list if all validations pass.
389
556
  Messages indicate type mismatches for known parameters.
390
-
557
+
391
558
  Example:
392
559
  >>> # Valid parameters
393
560
  >>> params = {"t0": 2459123.5, "u0": 0.1, "tE": 20.0}
394
561
  >>> messages = validate_parameter_types(params, "1S1L")
395
562
  >>> print(messages)
396
563
  []
397
-
564
+
398
565
  >>> # Invalid type for t0
399
566
  >>> params = {"t0": "2459123.5", "u0": 0.1, "tE": 20.0} # t0 is string
400
567
  >>> messages = validate_parameter_types(params, "1S1L")
401
568
  >>> print(messages)
402
569
  ["Parameter 't0' should be numeric, got str"]
403
-
570
+
404
571
  >>> # Unknown parameter (no validation performed)
405
572
  >>> params = {"t0": 2459123.5, "custom_param": "value"}
406
573
  >>> messages = validate_parameter_types(params, "1S1L")
407
574
  >>> print(messages)
408
575
  []
409
-
576
+
410
577
  Note:
411
578
  This function only validates parameters that are defined in
412
579
  PARAMETER_PROPERTIES. Unknown parameters are ignored to allow
@@ -414,50 +581,49 @@ def validate_parameter_types(
414
581
  is currently limited to basic type checking (float, int, str).
415
582
  """
416
583
  messages = []
417
-
584
+
418
585
  if model_type not in MODEL_DEFINITIONS:
419
586
  return [f"Unknown model type: '{model_type}'"]
420
-
587
+
421
588
  for param, value in parameters.items():
422
589
  if param in PARAMETER_PROPERTIES:
423
590
  prop = PARAMETER_PROPERTIES[param]
424
-
591
+
425
592
  # Check type
426
- expected_type = prop.get('type')
427
- if expected_type == 'float' and not isinstance(value, (int, float)):
428
- messages.append(f"Parameter '{param}' should be numeric, got {type(value).__name__}")
429
- elif expected_type == 'int' and not isinstance(value, int):
430
- messages.append(f"Parameter '{param}' should be integer, got {type(value).__name__}")
431
- elif expected_type == 'str' and not isinstance(value, str):
432
- messages.append(f"Parameter '{param}' should be string, got {type(value).__name__}")
433
-
593
+ expected_type = prop.get("type")
594
+ if expected_type == "float" and not isinstance(value, (int, float)):
595
+ messages.append(f"Parameter '{param}' should be numeric, got " f"{type(value).__name__}")
596
+ elif expected_type == "int" and not isinstance(value, int):
597
+ messages.append(f"Parameter '{param}' should be integer, got " f"{type(value).__name__}")
598
+ elif expected_type == "str" and not isinstance(value, str):
599
+ messages.append(f"Parameter '{param}' should be string, got " f"{type(value).__name__}")
600
+
434
601
  return messages
435
602
 
436
603
 
437
604
  def validate_parameter_uncertainties(
438
- parameters: Dict[str, Any],
439
- uncertainties: Optional[Dict[str, Any]] = None
605
+ parameters: Dict[str, Any], uncertainties: Optional[Dict[str, Any]] = None
440
606
  ) -> List[str]:
441
607
  """Validate parameter uncertainties for reasonableness and consistency.
442
-
608
+
443
609
  Performs comprehensive validation of parameter uncertainties, including:
444
610
  - Format validation (single value or [lower, upper] pairs)
445
611
  - Sign validation (uncertainties must be positive)
446
612
  - Consistency checks (lower ≤ upper for asymmetric uncertainties)
447
613
  - Reasonableness checks (relative uncertainty between 0.1% and 50%)
448
-
614
+
449
615
  Args:
450
616
  parameters: Dictionary of model parameters with parameter names as keys.
451
617
  uncertainties: Dictionary of parameter uncertainties. Can be None if
452
618
  no uncertainties are provided. Supports two formats:
453
619
  - Single value: {"param": 0.1} (symmetric uncertainty)
454
620
  - Asymmetric bounds: {"param": [0.05, 0.15]} (lower, upper)
455
-
621
+
456
622
  Returns:
457
623
  List of validation messages. Empty list if all validations pass.
458
624
  Messages indicate format errors, sign issues, consistency problems,
459
625
  or warnings about very large/small relative uncertainties.
460
-
626
+
461
627
  Example:
462
628
  >>> # Valid symmetric uncertainties
463
629
  >>> params = {"t0": 2459123.5, "u0": 0.1, "tE": 20.0}
@@ -465,31 +631,31 @@ def validate_parameter_uncertainties(
465
631
  >>> messages = validate_parameter_uncertainties(params, unc)
466
632
  >>> print(messages)
467
633
  []
468
-
634
+
469
635
  >>> # Valid asymmetric uncertainties
470
636
  >>> unc = {"t0": [0.05, 0.15], "u0": [0.005, 0.015]}
471
637
  >>> messages = validate_parameter_uncertainties(params, unc)
472
638
  >>> print(messages)
473
639
  []
474
-
640
+
475
641
  >>> # Invalid format
476
642
  >>> unc = {"t0": [0.1, 0.2, 0.3]} # Too many values
477
643
  >>> messages = validate_parameter_uncertainties(params, unc)
478
644
  >>> print(messages)
479
645
  ["Uncertainty for 't0' should be [lower, upper] or single value"]
480
-
646
+
481
647
  >>> # Inconsistent bounds
482
648
  >>> unc = {"t0": [0.2, 0.1]} # Lower > upper
483
649
  >>> messages = validate_parameter_uncertainties(params, unc)
484
650
  >>> print(messages)
485
651
  ["Lower uncertainty for 't0' (0.2) > upper uncertainty (0.1)"]
486
-
652
+
487
653
  >>> # Very large relative uncertainty
488
654
  >>> unc = {"t0": 1000.0} # Very large uncertainty
489
655
  >>> messages = validate_parameter_uncertainties(params, unc)
490
656
  >>> print(messages)
491
657
  ["Warning: Uncertainty for 't0' is very large (40.8% of parameter value)"]
492
-
658
+
493
659
  Note:
494
660
  This function provides warnings rather than errors for very large
495
661
  or very small relative uncertainties, as these might be legitimate
@@ -497,22 +663,22 @@ def validate_parameter_uncertainties(
497
663
  typical microlensing parameter uncertainties.
498
664
  """
499
665
  messages = []
500
-
666
+
501
667
  if not uncertainties:
502
668
  return messages
503
-
669
+
504
670
  for param_name, uncertainty in uncertainties.items():
505
671
  if param_name not in parameters:
506
672
  messages.append(f"Uncertainty provided for unknown parameter '{param_name}'")
507
673
  continue
508
-
674
+
509
675
  param_value = parameters[param_name]
510
-
676
+
511
677
  # Handle different uncertainty formats
512
678
  if isinstance(uncertainty, (list, tuple)):
513
679
  # [lower, upper] format
514
680
  if len(uncertainty) != 2:
515
- messages.append(f"Uncertainty for '{param_name}' should be [lower, upper] or single value")
681
+ messages.append(f"Uncertainty for '{param_name}' should be [lower, upper] " f"or single value")
516
682
  continue
517
683
  lower, upper = uncertainty
518
684
  if not (isinstance(lower, (int, float)) and isinstance(upper, (int, float))):
@@ -522,7 +688,7 @@ def validate_parameter_uncertainties(
522
688
  messages.append(f"Uncertainty bounds for '{param_name}' must be positive")
523
689
  continue
524
690
  if lower > upper:
525
- messages.append(f"Lower uncertainty for '{param_name}' ({lower}) > upper uncertainty ({upper})")
691
+ messages.append(f"Lower uncertainty for '{param_name}' ({lower}) > " f"upper uncertainty ({upper})")
526
692
  continue
527
693
  else:
528
694
  # Single value format
@@ -533,21 +699,30 @@ def validate_parameter_uncertainties(
533
699
  messages.append(f"Uncertainty for '{param_name}' must be positive")
534
700
  continue
535
701
  lower = upper = uncertainty
536
-
702
+
537
703
  # Check if uncertainty is reasonable relative to parameter value
538
704
  if isinstance(param_value, (int, float)) and param_value != 0:
539
705
  # Calculate relative uncertainty
540
706
  if isinstance(uncertainty, (list, tuple)):
541
- rel_uncertainty = max(abs(lower/param_value), abs(upper/param_value))
707
+ rel_uncertainty = max(
708
+ abs(lower / param_value),
709
+ abs(upper / param_value),
710
+ )
542
711
  else:
543
- rel_uncertainty = abs(uncertainty/param_value)
544
-
712
+ rel_uncertainty = abs(uncertainty / param_value)
713
+
545
714
  # Warn if uncertainty is very large (>50%) or very small (<0.1%)
546
715
  if rel_uncertainty > 0.5:
547
- messages.append(f"Warning: Uncertainty for '{param_name}' is very large ({rel_uncertainty:.1%} of parameter value)")
716
+ messages.append(
717
+ f"Warning: Uncertainty for '{param_name}' is very large "
718
+ f"({rel_uncertainty:.1%} of parameter value)"
719
+ )
548
720
  elif rel_uncertainty < 0.001:
549
- messages.append(f"Warning: Uncertainty for '{param_name}' is very small ({rel_uncertainty:.1%} of parameter value)")
550
-
721
+ messages.append(
722
+ f"Warning: Uncertainty for '{param_name}' is very small "
723
+ f"({rel_uncertainty:.1%} of parameter value)"
724
+ )
725
+
551
726
  return messages
552
727
 
553
728
 
@@ -557,52 +732,52 @@ def validate_solution_consistency(
557
732
  **kwargs: Any,
558
733
  ) -> List[str]:
559
734
  """Validate internal consistency of solution parameters.
560
-
735
+
561
736
  Performs physical consistency checks on microlensing parameters to
562
737
  identify potentially problematic values. This includes range validation,
563
738
  physical constraints, and model-specific consistency checks.
564
-
739
+
565
740
  Args:
566
741
  model_type: The type of microlensing model (e.g., '1S1L', '1S2L').
567
742
  parameters: Dictionary of model parameters with parameter names as keys.
568
743
  **kwargs: Additional solution attributes. Currently supports:
569
744
  relative_probability: Probability value for range checking (0-1).
570
-
745
+
571
746
  Returns:
572
747
  List of validation messages. Empty list if all validations pass.
573
748
  Messages indicate physical inconsistencies, range violations,
574
749
  or warnings about unusual parameter combinations.
575
-
750
+
576
751
  Example:
577
752
  >>> # Valid parameters
578
753
  >>> params = {"t0": 2459123.5, "u0": 0.1, "tE": 20.0}
579
754
  >>> messages = validate_solution_consistency("1S1L", params)
580
755
  >>> print(messages)
581
756
  []
582
-
757
+
583
758
  >>> # Invalid tE (must be positive)
584
759
  >>> params = {"t0": 2459123.5, "u0": 0.1, "tE": -5.0}
585
760
  >>> messages = validate_solution_consistency("1S1L", params)
586
761
  >>> print(messages)
587
762
  ["Einstein crossing time (tE) must be positive"]
588
-
763
+
589
764
  >>> # Invalid mass ratio
590
765
  >>> params = {"t0": 2459123.5, "u0": 0.1, "tE": 20.0, "q": 1.5}
591
766
  >>> messages = validate_solution_consistency("1S2L", params)
592
767
  >>> print(messages)
593
768
  ["Mass ratio (q) should be between 0 and 1"]
594
-
769
+
595
770
  >>> # Invalid relative probability
596
771
  >>> messages = validate_solution_consistency("1S1L", params, relative_probability=1.5)
597
772
  >>> print(messages)
598
773
  ["Relative probability should be between 0 and 1"]
599
-
774
+
600
775
  >>> # Binary lens with unusual separation
601
776
  >>> params = {"t0": 2459123.5, "u0": 0.1, "tE": 20.0, "s": 0.1, "q": 0.5}
602
777
  >>> messages = validate_solution_consistency("1S2L", params)
603
778
  >>> print(messages)
604
779
  ["Warning: Separation (s) outside typical caustic crossing range (0.5-2.0)"]
605
-
780
+
606
781
  Note:
607
782
  This function focuses on physical consistency rather than statistical
608
783
  validation. Warnings are provided for unusual but not impossible
@@ -610,30 +785,153 @@ def validate_solution_consistency(
610
785
  lenses is a guideline based on typical microlensing events.
611
786
  """
612
787
  messages = []
613
-
788
+
614
789
  # Check for physically impossible values
615
- if 'tE' in parameters and parameters['tE'] <= 0:
790
+ if "tE" in parameters and parameters["tE"] <= 0:
616
791
  messages.append("Einstein crossing time (tE) must be positive")
617
-
618
- if 'q' in parameters and (parameters['q'] <= 0 or parameters['q'] > 1):
792
+
793
+ if "q" in parameters and (parameters["q"] <= 0 or parameters["q"] > 1):
619
794
  messages.append("Mass ratio (q) should be between 0 and 1")
620
-
621
- if 's' in parameters and parameters['s'] <= 0:
795
+
796
+ if "s" in parameters and parameters["s"] <= 0:
622
797
  messages.append("Separation (s) must be positive")
623
798
 
624
799
  rel_prob = kwargs.get("relative_probability")
625
800
  if rel_prob is not None and not 0 <= rel_prob <= 1:
626
801
  messages.append("Relative probability should be between 0 and 1")
627
-
802
+
628
803
  # Check for binary lens specific consistency (1S2L, 2S2L models)
629
- if model_type in ['1S2L', '2S2L']:
630
- if 'q' in parameters and 's' in parameters:
804
+ if model_type in ["1S2L", "2S2L"]:
805
+ if "q" in parameters and "s" in parameters:
631
806
  # Check for caustic crossing conditions
632
- q = parameters['q']
633
- s = parameters['s']
634
-
807
+ s = parameters["s"]
808
+
635
809
  # Simple caustic crossing check
636
810
  if s < 0.5 or s > 2.0:
637
- messages.append("Warning: Separation (s) outside typical caustic crossing range (0.5-2.0)")
638
-
811
+ messages.append("Warning: " "Separation (s) outside typical caustic crossing range " "(0.5-2.0)")
812
+
813
+ return messages
814
+
815
+
816
+ def validate_solution_rigorously(
817
+ model_type: str,
818
+ parameters: Dict[str, Any],
819
+ higher_order_effects: Optional[List[str]] = None,
820
+ bands: Optional[List[str]] = None,
821
+ t_ref: Optional[float] = None,
822
+ ) -> List[str]:
823
+ """Extremely rigorous validation of solution parameters.
824
+
825
+ This function performs comprehensive validation that catches ALL parameter errors:
826
+ - Parameter types must be correct (t_ref must be float, etc.)
827
+ - No invalid parameters for model type (e.g., 's' parameter for 1S1L)
828
+ - t_ref only allowed when required by higher-order effects
829
+ - bands must be a list of strings
830
+ - All required flux parameters must be present for each band
831
+ - Only "other" model types or effects can have unknown parameters
832
+
833
+ Args:
834
+ model_type: The type of microlensing model
835
+ parameters: Dictionary of model parameters
836
+ higher_order_effects: List of higher-order effects
837
+ bands: List of photometric bands
838
+ t_ref: Reference time for time-dependent effects
839
+
840
+ Returns:
841
+ List of validation error messages. Empty list if all validations pass.
842
+ """
843
+ messages = []
844
+ higher_order_effects = higher_order_effects or []
845
+ bands = bands or []
846
+
847
+ # 1. Validate t_ref type
848
+ if t_ref is not None and not isinstance(t_ref, (int, float)):
849
+ messages.append(f"t_ref must be numeric, got {type(t_ref).__name__}")
850
+
851
+ # 2. Validate bands format
852
+ if not isinstance(bands, list):
853
+ messages.append(f"bands must be a list, got {type(bands).__name__}")
854
+ else:
855
+ for i, band in enumerate(bands):
856
+ if not isinstance(band, str):
857
+ messages.append(f"band {i} must be a string, got {type(band).__name__}")
858
+
859
+ # 3. Check if t_ref is provided when not needed
860
+ t_ref_required = False
861
+ for effect in higher_order_effects:
862
+ if effect in HIGHER_ORDER_EFFECT_DEFINITIONS:
863
+ if HIGHER_ORDER_EFFECT_DEFINITIONS[effect].get("requires_t_ref", False):
864
+ t_ref_required = True
865
+ break
866
+
867
+ if not t_ref_required and t_ref is not None:
868
+ messages.append("t_ref provided but not required by any higher-order effects")
869
+
870
+ # 4. Get all valid parameters for this model and effects
871
+ valid_params = set()
872
+
873
+ # Add core model parameters
874
+ if model_type in MODEL_DEFINITIONS:
875
+ valid_params.update(MODEL_DEFINITIONS[model_type]["required_params_core"])
876
+ elif model_type != "other":
877
+ messages.append(f"Unknown model type: '{model_type}'")
878
+
879
+ # Add higher-order effect parameters
880
+ for effect in higher_order_effects:
881
+ if effect in HIGHER_ORDER_EFFECT_DEFINITIONS:
882
+ effect_def = HIGHER_ORDER_EFFECT_DEFINITIONS[effect]
883
+ valid_params.update(effect_def.get("required_higher_order_params", []))
884
+ valid_params.update(effect_def.get("optional_higher_order_params", []))
885
+ elif effect != "other":
886
+ messages.append(f"Unknown higher-order effect: '{effect}'")
887
+
888
+ # Add band-specific parameters
889
+ if bands:
890
+ valid_params.update(get_required_flux_params(model_type, bands))
891
+
892
+ # 5. Check for invalid parameters (unless model_type or effects are "other")
893
+ if model_type != "other" and "other" not in higher_order_effects:
894
+ invalid_params = set(parameters.keys()) - valid_params
895
+ for param in invalid_params:
896
+ messages.append(f"Invalid parameter '{param}' for model type '{model_type}'")
897
+
898
+ # 6. Validate parameter types for all parameters
899
+ for param, value in parameters.items():
900
+ if param in PARAMETER_PROPERTIES:
901
+ prop = PARAMETER_PROPERTIES[param]
902
+ expected_type = prop.get("type")
903
+
904
+ if expected_type == "float" and not isinstance(value, (int, float)):
905
+ messages.append(f"Parameter '{param}' must be numeric, got {type(value).__name__}")
906
+ elif expected_type == "int" and not isinstance(value, int):
907
+ messages.append(f"Parameter '{param}' must be integer, got {type(value).__name__}")
908
+ elif expected_type == "str" and not isinstance(value, str):
909
+ messages.append(f"Parameter '{param}' must be string, got {type(value).__name__}")
910
+
911
+ # 7. Check for missing required parameters
912
+ missing_core = []
913
+ if model_type in MODEL_DEFINITIONS:
914
+ for param in MODEL_DEFINITIONS[model_type]["required_params_core"]:
915
+ if param not in parameters:
916
+ missing_core.append(param)
917
+
918
+ if missing_core:
919
+ messages.append(f"Missing required parameters for {model_type}: {missing_core}")
920
+
921
+ # 8. Check for missing higher-order effect parameters
922
+ for effect in higher_order_effects:
923
+ if effect in HIGHER_ORDER_EFFECT_DEFINITIONS:
924
+ effect_def = HIGHER_ORDER_EFFECT_DEFINITIONS[effect]
925
+ required_params = effect_def.get("required_higher_order_params", [])
926
+ missing_params = [param for param in required_params if param not in parameters]
927
+ if missing_params:
928
+ messages.append(f"Missing required parameters for effect '{effect}': {missing_params}")
929
+
930
+ # 9. Check for missing flux parameters
931
+ if bands:
932
+ required_flux = get_required_flux_params(model_type, bands)
933
+ missing_flux = [param for param in required_flux if param not in parameters]
934
+ if missing_flux:
935
+ messages.append(f"Missing required flux parameters for bands {bands}: {missing_flux}")
936
+
639
937
  return messages