microlens-submit 0.12.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.
- microlens_submit/__init__.py +163 -0
- microlens_submit/api.py +1274 -0
- microlens_submit/assets/github-desktop_logo.png +0 -0
- microlens_submit/assets/rges-pit_logo.png +0 -0
- microlens_submit/cli.py +1803 -0
- microlens_submit/dossier.py +1443 -0
- microlens_submit/validate_parameters.py +639 -0
- microlens_submit-0.12.1.dist-info/METADATA +159 -0
- microlens_submit-0.12.1.dist-info/RECORD +13 -0
- microlens_submit-0.12.1.dist-info/WHEEL +5 -0
- microlens_submit-0.12.1.dist-info/entry_points.txt +2 -0
- microlens_submit-0.12.1.dist-info/licenses/LICENSE +21 -0
- microlens_submit-0.12.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parameter validation module for microlens-submit.
|
|
3
|
+
|
|
4
|
+
This module provides centralized validation logic for checking solution completeness
|
|
5
|
+
and parameter consistency against model definitions. It validates microlensing
|
|
6
|
+
solutions against predefined model types, higher-order effects, and parameter
|
|
7
|
+
constraints to ensure submissions are complete and physically reasonable.
|
|
8
|
+
|
|
9
|
+
The module defines:
|
|
10
|
+
- Model definitions with required parameters for each model type
|
|
11
|
+
- Higher-order effect definitions with associated parameters
|
|
12
|
+
- Parameter properties including types, units, and descriptions
|
|
13
|
+
- Validation functions for completeness, types, uncertainties, and consistency
|
|
14
|
+
|
|
15
|
+
**Supported Model Types:**
|
|
16
|
+
- 1S1L: Point Source, Single Point Lens (standard microlensing)
|
|
17
|
+
- 1S2L: Point Source, Binary Point Lens
|
|
18
|
+
- 2S1L: Binary Source, Single Point Lens
|
|
19
|
+
- 2S2L: Binary Source, Binary Point Lens (commented)
|
|
20
|
+
- 1S3L: Point Source, Triple Point Lens (commented)
|
|
21
|
+
- 2S3L: Binary Source, Triple Point Lens (commented)
|
|
22
|
+
|
|
23
|
+
**Supported Higher-Order Effects:**
|
|
24
|
+
- parallax: Microlens parallax effect
|
|
25
|
+
- finite-source: Finite source size effect
|
|
26
|
+
- lens-orbital-motion: Orbital motion of lens components
|
|
27
|
+
- xallarap: Source orbital motion
|
|
28
|
+
- gaussian-process: Gaussian process noise modeling
|
|
29
|
+
- stellar-rotation: Stellar rotation effects
|
|
30
|
+
- fitted-limb-darkening: Fitted limb darkening coefficients
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
>>> from microlens_submit.validate_parameters import check_solution_completeness
|
|
34
|
+
>>>
|
|
35
|
+
>>> # Validate a simple 1S1L solution
|
|
36
|
+
>>> parameters = {"t0": 2459123.5, "u0": 0.1, "tE": 20.0}
|
|
37
|
+
>>> messages = check_solution_completeness("1S1L", parameters)
|
|
38
|
+
>>> if not messages:
|
|
39
|
+
... print("Solution is complete!")
|
|
40
|
+
>>> else:
|
|
41
|
+
... print("Issues found:", messages)
|
|
42
|
+
|
|
43
|
+
>>> # Validate a binary lens with parallax
|
|
44
|
+
>>> parameters = {
|
|
45
|
+
... "t0": 2459123.5, "u0": 0.1, "tE": 20.0,
|
|
46
|
+
... "s": 1.2, "q": 0.5, "alpha": 45.0,
|
|
47
|
+
... "piEN": 0.1, "piEE": 0.05
|
|
48
|
+
... }
|
|
49
|
+
>>> effects = ["parallax"]
|
|
50
|
+
>>> messages = check_solution_completeness("1S2L", parameters, effects, t_ref=2459123.0)
|
|
51
|
+
>>> print("Validation messages:", messages)
|
|
52
|
+
|
|
53
|
+
Note:
|
|
54
|
+
All validation functions return lists of human-readable messages instead
|
|
55
|
+
of raising exceptions, allowing for comprehensive validation reporting.
|
|
56
|
+
Unknown parameters generate warnings rather than errors to accommodate
|
|
57
|
+
custom parameters and future model types.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
from typing import Dict, List, Any, Optional, Union
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
MODEL_DEFINITIONS = {
|
|
64
|
+
# Single Source, Single Lens (PSPL)
|
|
65
|
+
"1S1L": {
|
|
66
|
+
"description": "Point Source, Single Point Lens (standard microlensing)",
|
|
67
|
+
"required_params_core": ["t0", "u0", "tE"],
|
|
68
|
+
},
|
|
69
|
+
# Single Source, Binary Lens
|
|
70
|
+
"1S2L": {
|
|
71
|
+
"description": "Point Source, Binary Point Lens",
|
|
72
|
+
"required_params_core": ["t0", "u0", "tE", "s", "q", "alpha"],
|
|
73
|
+
},
|
|
74
|
+
# Binary Source, Single Lens
|
|
75
|
+
"2S1L": {
|
|
76
|
+
"description": "Binary Source, Single Point Lens",
|
|
77
|
+
"required_params_core": ["t0", "u0", "tE"], # Core lens params
|
|
78
|
+
},
|
|
79
|
+
# 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"]},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
HIGHER_ORDER_EFFECT_DEFINITIONS = {
|
|
86
|
+
"parallax": {
|
|
87
|
+
"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
|
|
90
|
+
},
|
|
91
|
+
"finite-source": {
|
|
92
|
+
"description": "Finite source size effect",
|
|
93
|
+
"requires_t_ref": False,
|
|
94
|
+
"required_higher_order_params": ["rho"],
|
|
95
|
+
},
|
|
96
|
+
"lens-orbital-motion": {
|
|
97
|
+
"description": "Orbital motion of the lens components",
|
|
98
|
+
"requires_t_ref": True,
|
|
99
|
+
"required_higher_order_params": ["dsdt", "dadt"],
|
|
100
|
+
"optional_higher_order_params": ["dzdt"], # Relative radial rate of change of lenses (if needed)
|
|
101
|
+
},
|
|
102
|
+
"xallarap": {
|
|
103
|
+
"description": "Source orbital motion (xallarap)",
|
|
104
|
+
"requires_t_ref": True, # Xallarap often has a t_ref related to its epoch
|
|
105
|
+
"required_higher_order_params": [], # Specific parameters (e.g., orbital period, inclination) to be added here
|
|
106
|
+
},
|
|
107
|
+
"gaussian-process": {
|
|
108
|
+
"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.
|
|
112
|
+
},
|
|
113
|
+
"stellar-rotation": {
|
|
114
|
+
"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
|
|
118
|
+
},
|
|
119
|
+
"fitted-limb-darkening": {
|
|
120
|
+
"description": "Limb darkening coefficients fitted as parameters",
|
|
121
|
+
"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)
|
|
124
|
+
},
|
|
125
|
+
# The "other" effect type is handled by allowing any other string in `higher_order_effects` list itself.
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# This dictionary defines properties/constraints for each known parameter
|
|
129
|
+
# (e.g., expected type, units, a more detailed description, corresponding uncertainty field name)
|
|
130
|
+
PARAMETER_PROPERTIES = {
|
|
131
|
+
# Core Microlensing Parameters
|
|
132
|
+
"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"},
|
|
136
|
+
"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
|
+
|
|
139
|
+
# 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
|
+
|
|
147
|
+
# Flux Parameters (dynamically generated by get_required_flux_params)
|
|
148
|
+
# 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
|
+
|
|
156
|
+
# 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
|
+
|
|
164
|
+
# 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
|
+
|
|
170
|
+
# 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
|
+
|
|
174
|
+
# 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)"},
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
def get_required_flux_params(model_type: str, bands: List[str]) -> List[str]:
|
|
182
|
+
"""Get the required flux parameters for a given model type and bands.
|
|
183
|
+
|
|
184
|
+
Determines which flux parameters are required based on the model type
|
|
185
|
+
(single vs binary source) and the photometric bands used. For single
|
|
186
|
+
source models, each band requires source and blend flux parameters.
|
|
187
|
+
For binary source models, each band requires two source fluxes and
|
|
188
|
+
a common blend flux.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
model_type: The type of microlensing model (e.g., "1S1L", "2S1L").
|
|
192
|
+
bands: List of band IDs as strings (e.g., ["0", "1", "2"]).
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
List of required flux parameter names (e.g., ["F0_S", "F0_B", "F1_S", "F1_B"]).
|
|
196
|
+
|
|
197
|
+
Example:
|
|
198
|
+
>>> get_required_flux_params("1S1L", ["0", "1"])
|
|
199
|
+
['F0_S', 'F0_B', 'F1_S', 'F1_B']
|
|
200
|
+
|
|
201
|
+
>>> get_required_flux_params("2S1L", ["0", "1"])
|
|
202
|
+
['F0_S1', 'F0_S2', 'F0_B', 'F1_S1', 'F1_S2', 'F1_B']
|
|
203
|
+
|
|
204
|
+
>>> get_required_flux_params("1S1L", [])
|
|
205
|
+
[]
|
|
206
|
+
|
|
207
|
+
Note:
|
|
208
|
+
This function handles the most common model types (1S and 2S).
|
|
209
|
+
For models with more than 2 sources, additional logic would be needed.
|
|
210
|
+
The function returns an empty list if no bands are specified.
|
|
211
|
+
"""
|
|
212
|
+
flux_params = []
|
|
213
|
+
if not bands:
|
|
214
|
+
return flux_params # No bands, no flux parameters
|
|
215
|
+
|
|
216
|
+
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)
|
|
224
|
+
# Add more source types (e.g., 3S) if necessary in the future
|
|
225
|
+
return flux_params
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def check_solution_completeness(
|
|
229
|
+
model_type: str,
|
|
230
|
+
parameters: Dict[str, Any],
|
|
231
|
+
higher_order_effects: Optional[List[str]] = None,
|
|
232
|
+
bands: Optional[List[str]] = None,
|
|
233
|
+
t_ref: Optional[float] = None,
|
|
234
|
+
**kwargs
|
|
235
|
+
) -> List[str]:
|
|
236
|
+
"""Check if a solution has all required parameters based on its model type and effects.
|
|
237
|
+
|
|
238
|
+
This function validates that all required parameters are present for the given
|
|
239
|
+
model type and any higher-order effects. It returns a list of human-readable
|
|
240
|
+
warning or error messages instead of raising exceptions immediately.
|
|
241
|
+
|
|
242
|
+
The validation checks:
|
|
243
|
+
- Required core parameters for the model type
|
|
244
|
+
- Required parameters for each higher-order effect
|
|
245
|
+
- Flux parameters for specified photometric bands
|
|
246
|
+
- Reference time requirements for time-dependent effects
|
|
247
|
+
- Recognition of unknown parameters (warnings only)
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
model_type: The type of microlensing model (e.g., '1S1L', '1S2L').
|
|
251
|
+
parameters: Dictionary of model parameters with parameter names as keys.
|
|
252
|
+
higher_order_effects: List of higher-order effects (e.g., ['parallax', 'finite-source']).
|
|
253
|
+
If None, no higher-order effects are assumed.
|
|
254
|
+
bands: List of photometric bands used (e.g., ["0", "1", "2"]).
|
|
255
|
+
If None, no band-specific parameters are required.
|
|
256
|
+
t_ref: Reference time for time-dependent effects (Julian Date).
|
|
257
|
+
Required for effects that specify requires_t_ref=True.
|
|
258
|
+
**kwargs: Additional solution attributes to validate (currently unused).
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
List of validation messages. Empty list if all validations pass.
|
|
262
|
+
Messages indicate missing required parameters, unknown effects,
|
|
263
|
+
missing reference times, or unrecognized parameters.
|
|
264
|
+
|
|
265
|
+
Example:
|
|
266
|
+
>>> # Simple 1S1L solution - should pass
|
|
267
|
+
>>> params = {"t0": 2459123.5, "u0": 0.1, "tE": 20.0}
|
|
268
|
+
>>> messages = check_solution_completeness("1S1L", params)
|
|
269
|
+
>>> print(messages)
|
|
270
|
+
[]
|
|
271
|
+
|
|
272
|
+
>>> # Missing required parameter
|
|
273
|
+
>>> params = {"t0": 2459123.5, "u0": 0.1} # Missing tE
|
|
274
|
+
>>> messages = check_solution_completeness("1S1L", params)
|
|
275
|
+
>>> print(messages)
|
|
276
|
+
["Missing required core parameter 'tE' for model type '1S1L'"]
|
|
277
|
+
|
|
278
|
+
>>> # Binary lens with parallax
|
|
279
|
+
>>> params = {
|
|
280
|
+
... "t0": 2459123.5, "u0": 0.1, "tE": 20.0,
|
|
281
|
+
... "s": 1.2, "q": 0.5, "alpha": 45.0,
|
|
282
|
+
... "piEN": 0.1, "piEE": 0.05
|
|
283
|
+
... }
|
|
284
|
+
>>> messages = check_solution_completeness("1S2L", params, ["parallax"], t_ref=2459123.0)
|
|
285
|
+
>>> print(messages)
|
|
286
|
+
[]
|
|
287
|
+
|
|
288
|
+
>>> # Missing reference time for parallax
|
|
289
|
+
>>> messages = check_solution_completeness("1S2L", params, ["parallax"])
|
|
290
|
+
>>> print(messages)
|
|
291
|
+
["Reference time (t_ref) required for effect 'parallax'"]
|
|
292
|
+
|
|
293
|
+
Note:
|
|
294
|
+
This function is designed to be comprehensive but not overly strict.
|
|
295
|
+
Unknown parameters generate warnings rather than errors to accommodate
|
|
296
|
+
custom parameters and future model types. The function validates against
|
|
297
|
+
the predefined MODEL_DEFINITIONS and HIGHER_ORDER_EFFECT_DEFINITIONS.
|
|
298
|
+
"""
|
|
299
|
+
messages = []
|
|
300
|
+
|
|
301
|
+
# Validate model type
|
|
302
|
+
if model_type not in MODEL_DEFINITIONS:
|
|
303
|
+
messages.append(f"Unknown model type: '{model_type}'. Valid types: {list(MODEL_DEFINITIONS.keys())}")
|
|
304
|
+
return messages
|
|
305
|
+
|
|
306
|
+
model_def = MODEL_DEFINITIONS[model_type]
|
|
307
|
+
|
|
308
|
+
# Check required core parameters
|
|
309
|
+
required_core_params = model_def.get('required_params_core', [])
|
|
310
|
+
for param in required_core_params:
|
|
311
|
+
if param not in parameters:
|
|
312
|
+
messages.append(f"Missing required core parameter '{param}' for model type '{model_type}'")
|
|
313
|
+
|
|
314
|
+
# Validate higher-order effects
|
|
315
|
+
if higher_order_effects:
|
|
316
|
+
for effect in higher_order_effects:
|
|
317
|
+
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())}")
|
|
319
|
+
continue
|
|
320
|
+
|
|
321
|
+
effect_def = HIGHER_ORDER_EFFECT_DEFINITIONS[effect]
|
|
322
|
+
|
|
323
|
+
# Check required parameters for this effect
|
|
324
|
+
effect_required = effect_def.get('required_higher_order_params', [])
|
|
325
|
+
for param in effect_required:
|
|
326
|
+
if param not in parameters:
|
|
327
|
+
messages.append(f"Missing required parameter '{param}' for effect '{effect}'")
|
|
328
|
+
|
|
329
|
+
# Check optional parameters for this effect
|
|
330
|
+
effect_optional = effect_def.get('optional_higher_order_params', [])
|
|
331
|
+
for param in effect_optional:
|
|
332
|
+
if param not in parameters:
|
|
333
|
+
messages.append(f"Warning: Optional parameter '{param}' not provided for effect '{effect}'")
|
|
334
|
+
|
|
335
|
+
# Check if t_ref is required for this effect
|
|
336
|
+
if effect_def.get('requires_t_ref', False) and t_ref is None:
|
|
337
|
+
messages.append(f"Reference time (t_ref) required for effect '{effect}'")
|
|
338
|
+
|
|
339
|
+
# Validate band-specific parameters
|
|
340
|
+
if bands:
|
|
341
|
+
required_flux_params = get_required_flux_params(model_type, bands)
|
|
342
|
+
for param in required_flux_params:
|
|
343
|
+
if param not in parameters:
|
|
344
|
+
messages.append(f"Missing required flux parameter '{param}' for bands {bands}")
|
|
345
|
+
|
|
346
|
+
# Check for invalid parameters (not in any definition)
|
|
347
|
+
all_valid_params = set()
|
|
348
|
+
|
|
349
|
+
# Add core model parameters
|
|
350
|
+
all_valid_params.update(required_core_params)
|
|
351
|
+
|
|
352
|
+
# Add higher-order effect parameters
|
|
353
|
+
if higher_order_effects:
|
|
354
|
+
for effect in higher_order_effects:
|
|
355
|
+
if effect in HIGHER_ORDER_EFFECT_DEFINITIONS:
|
|
356
|
+
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
|
+
|
|
360
|
+
# Add band-specific parameters if bands are specified
|
|
361
|
+
if bands:
|
|
362
|
+
all_valid_params.update(get_required_flux_params(model_type, bands))
|
|
363
|
+
|
|
364
|
+
# Check for invalid parameters
|
|
365
|
+
invalid_params = set(parameters.keys()) - all_valid_params
|
|
366
|
+
for param in invalid_params:
|
|
367
|
+
messages.append(f"Warning: Parameter '{param}' not recognized for model type '{model_type}'")
|
|
368
|
+
|
|
369
|
+
return messages
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def validate_parameter_types(
|
|
373
|
+
parameters: Dict[str, Any],
|
|
374
|
+
model_type: str
|
|
375
|
+
) -> List[str]:
|
|
376
|
+
"""Validate parameter types and value ranges against expected types.
|
|
377
|
+
|
|
378
|
+
Checks that parameters have the correct data types as defined in
|
|
379
|
+
PARAMETER_PROPERTIES. Currently supports validation of float, int,
|
|
380
|
+
and string types. Parameters not defined in PARAMETER_PROPERTIES
|
|
381
|
+
are skipped (no validation performed).
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
parameters: Dictionary of model parameters with parameter names as keys.
|
|
385
|
+
model_type: The type of microlensing model (used for context in messages).
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
List of validation messages. Empty list if all validations pass.
|
|
389
|
+
Messages indicate type mismatches for known parameters.
|
|
390
|
+
|
|
391
|
+
Example:
|
|
392
|
+
>>> # Valid parameters
|
|
393
|
+
>>> params = {"t0": 2459123.5, "u0": 0.1, "tE": 20.0}
|
|
394
|
+
>>> messages = validate_parameter_types(params, "1S1L")
|
|
395
|
+
>>> print(messages)
|
|
396
|
+
[]
|
|
397
|
+
|
|
398
|
+
>>> # Invalid type for t0
|
|
399
|
+
>>> params = {"t0": "2459123.5", "u0": 0.1, "tE": 20.0} # t0 is string
|
|
400
|
+
>>> messages = validate_parameter_types(params, "1S1L")
|
|
401
|
+
>>> print(messages)
|
|
402
|
+
["Parameter 't0' should be numeric, got str"]
|
|
403
|
+
|
|
404
|
+
>>> # Unknown parameter (no validation performed)
|
|
405
|
+
>>> params = {"t0": 2459123.5, "custom_param": "value"}
|
|
406
|
+
>>> messages = validate_parameter_types(params, "1S1L")
|
|
407
|
+
>>> print(messages)
|
|
408
|
+
[]
|
|
409
|
+
|
|
410
|
+
Note:
|
|
411
|
+
This function only validates parameters that are defined in
|
|
412
|
+
PARAMETER_PROPERTIES. Unknown parameters are ignored to allow
|
|
413
|
+
for custom parameters and future extensions. The validation
|
|
414
|
+
is currently limited to basic type checking (float, int, str).
|
|
415
|
+
"""
|
|
416
|
+
messages = []
|
|
417
|
+
|
|
418
|
+
if model_type not in MODEL_DEFINITIONS:
|
|
419
|
+
return [f"Unknown model type: '{model_type}'"]
|
|
420
|
+
|
|
421
|
+
for param, value in parameters.items():
|
|
422
|
+
if param in PARAMETER_PROPERTIES:
|
|
423
|
+
prop = PARAMETER_PROPERTIES[param]
|
|
424
|
+
|
|
425
|
+
# 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
|
+
|
|
434
|
+
return messages
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def validate_parameter_uncertainties(
|
|
438
|
+
parameters: Dict[str, Any],
|
|
439
|
+
uncertainties: Optional[Dict[str, Any]] = None
|
|
440
|
+
) -> List[str]:
|
|
441
|
+
"""Validate parameter uncertainties for reasonableness and consistency.
|
|
442
|
+
|
|
443
|
+
Performs comprehensive validation of parameter uncertainties, including:
|
|
444
|
+
- Format validation (single value or [lower, upper] pairs)
|
|
445
|
+
- Sign validation (uncertainties must be positive)
|
|
446
|
+
- Consistency checks (lower ≤ upper for asymmetric uncertainties)
|
|
447
|
+
- Reasonableness checks (relative uncertainty between 0.1% and 50%)
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
parameters: Dictionary of model parameters with parameter names as keys.
|
|
451
|
+
uncertainties: Dictionary of parameter uncertainties. Can be None if
|
|
452
|
+
no uncertainties are provided. Supports two formats:
|
|
453
|
+
- Single value: {"param": 0.1} (symmetric uncertainty)
|
|
454
|
+
- Asymmetric bounds: {"param": [0.05, 0.15]} (lower, upper)
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
List of validation messages. Empty list if all validations pass.
|
|
458
|
+
Messages indicate format errors, sign issues, consistency problems,
|
|
459
|
+
or warnings about very large/small relative uncertainties.
|
|
460
|
+
|
|
461
|
+
Example:
|
|
462
|
+
>>> # Valid symmetric uncertainties
|
|
463
|
+
>>> params = {"t0": 2459123.5, "u0": 0.1, "tE": 20.0}
|
|
464
|
+
>>> unc = {"t0": 0.1, "u0": 0.01, "tE": 0.5}
|
|
465
|
+
>>> messages = validate_parameter_uncertainties(params, unc)
|
|
466
|
+
>>> print(messages)
|
|
467
|
+
[]
|
|
468
|
+
|
|
469
|
+
>>> # Valid asymmetric uncertainties
|
|
470
|
+
>>> unc = {"t0": [0.05, 0.15], "u0": [0.005, 0.015]}
|
|
471
|
+
>>> messages = validate_parameter_uncertainties(params, unc)
|
|
472
|
+
>>> print(messages)
|
|
473
|
+
[]
|
|
474
|
+
|
|
475
|
+
>>> # Invalid format
|
|
476
|
+
>>> unc = {"t0": [0.1, 0.2, 0.3]} # Too many values
|
|
477
|
+
>>> messages = validate_parameter_uncertainties(params, unc)
|
|
478
|
+
>>> print(messages)
|
|
479
|
+
["Uncertainty for 't0' should be [lower, upper] or single value"]
|
|
480
|
+
|
|
481
|
+
>>> # Inconsistent bounds
|
|
482
|
+
>>> unc = {"t0": [0.2, 0.1]} # Lower > upper
|
|
483
|
+
>>> messages = validate_parameter_uncertainties(params, unc)
|
|
484
|
+
>>> print(messages)
|
|
485
|
+
["Lower uncertainty for 't0' (0.2) > upper uncertainty (0.1)"]
|
|
486
|
+
|
|
487
|
+
>>> # Very large relative uncertainty
|
|
488
|
+
>>> unc = {"t0": 1000.0} # Very large uncertainty
|
|
489
|
+
>>> messages = validate_parameter_uncertainties(params, unc)
|
|
490
|
+
>>> print(messages)
|
|
491
|
+
["Warning: Uncertainty for 't0' is very large (40.8% of parameter value)"]
|
|
492
|
+
|
|
493
|
+
Note:
|
|
494
|
+
This function provides warnings rather than errors for very large
|
|
495
|
+
or very small relative uncertainties, as these might be legitimate
|
|
496
|
+
in some cases. The 0.1% to 50% range is a guideline based on
|
|
497
|
+
typical microlensing parameter uncertainties.
|
|
498
|
+
"""
|
|
499
|
+
messages = []
|
|
500
|
+
|
|
501
|
+
if not uncertainties:
|
|
502
|
+
return messages
|
|
503
|
+
|
|
504
|
+
for param_name, uncertainty in uncertainties.items():
|
|
505
|
+
if param_name not in parameters:
|
|
506
|
+
messages.append(f"Uncertainty provided for unknown parameter '{param_name}'")
|
|
507
|
+
continue
|
|
508
|
+
|
|
509
|
+
param_value = parameters[param_name]
|
|
510
|
+
|
|
511
|
+
# Handle different uncertainty formats
|
|
512
|
+
if isinstance(uncertainty, (list, tuple)):
|
|
513
|
+
# [lower, upper] format
|
|
514
|
+
if len(uncertainty) != 2:
|
|
515
|
+
messages.append(f"Uncertainty for '{param_name}' should be [lower, upper] or single value")
|
|
516
|
+
continue
|
|
517
|
+
lower, upper = uncertainty
|
|
518
|
+
if not (isinstance(lower, (int, float)) and isinstance(upper, (int, float))):
|
|
519
|
+
messages.append(f"Uncertainty bounds for '{param_name}' must be numeric")
|
|
520
|
+
continue
|
|
521
|
+
if lower < 0 or upper < 0:
|
|
522
|
+
messages.append(f"Uncertainty bounds for '{param_name}' must be positive")
|
|
523
|
+
continue
|
|
524
|
+
if lower > upper:
|
|
525
|
+
messages.append(f"Lower uncertainty for '{param_name}' ({lower}) > upper uncertainty ({upper})")
|
|
526
|
+
continue
|
|
527
|
+
else:
|
|
528
|
+
# Single value format
|
|
529
|
+
if not isinstance(uncertainty, (int, float)):
|
|
530
|
+
messages.append(f"Uncertainty for '{param_name}' must be numeric")
|
|
531
|
+
continue
|
|
532
|
+
if uncertainty < 0:
|
|
533
|
+
messages.append(f"Uncertainty for '{param_name}' must be positive")
|
|
534
|
+
continue
|
|
535
|
+
lower = upper = uncertainty
|
|
536
|
+
|
|
537
|
+
# Check if uncertainty is reasonable relative to parameter value
|
|
538
|
+
if isinstance(param_value, (int, float)) and param_value != 0:
|
|
539
|
+
# Calculate relative uncertainty
|
|
540
|
+
if isinstance(uncertainty, (list, tuple)):
|
|
541
|
+
rel_uncertainty = max(abs(lower/param_value), abs(upper/param_value))
|
|
542
|
+
else:
|
|
543
|
+
rel_uncertainty = abs(uncertainty/param_value)
|
|
544
|
+
|
|
545
|
+
# Warn if uncertainty is very large (>50%) or very small (<0.1%)
|
|
546
|
+
if rel_uncertainty > 0.5:
|
|
547
|
+
messages.append(f"Warning: Uncertainty for '{param_name}' is very large ({rel_uncertainty:.1%} of parameter value)")
|
|
548
|
+
elif rel_uncertainty < 0.001:
|
|
549
|
+
messages.append(f"Warning: Uncertainty for '{param_name}' is very small ({rel_uncertainty:.1%} of parameter value)")
|
|
550
|
+
|
|
551
|
+
return messages
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def validate_solution_consistency(
|
|
555
|
+
model_type: str,
|
|
556
|
+
parameters: Dict[str, Any],
|
|
557
|
+
**kwargs: Any,
|
|
558
|
+
) -> List[str]:
|
|
559
|
+
"""Validate internal consistency of solution parameters.
|
|
560
|
+
|
|
561
|
+
Performs physical consistency checks on microlensing parameters to
|
|
562
|
+
identify potentially problematic values. This includes range validation,
|
|
563
|
+
physical constraints, and model-specific consistency checks.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
model_type: The type of microlensing model (e.g., '1S1L', '1S2L').
|
|
567
|
+
parameters: Dictionary of model parameters with parameter names as keys.
|
|
568
|
+
**kwargs: Additional solution attributes. Currently supports:
|
|
569
|
+
relative_probability: Probability value for range checking (0-1).
|
|
570
|
+
|
|
571
|
+
Returns:
|
|
572
|
+
List of validation messages. Empty list if all validations pass.
|
|
573
|
+
Messages indicate physical inconsistencies, range violations,
|
|
574
|
+
or warnings about unusual parameter combinations.
|
|
575
|
+
|
|
576
|
+
Example:
|
|
577
|
+
>>> # Valid parameters
|
|
578
|
+
>>> params = {"t0": 2459123.5, "u0": 0.1, "tE": 20.0}
|
|
579
|
+
>>> messages = validate_solution_consistency("1S1L", params)
|
|
580
|
+
>>> print(messages)
|
|
581
|
+
[]
|
|
582
|
+
|
|
583
|
+
>>> # Invalid tE (must be positive)
|
|
584
|
+
>>> params = {"t0": 2459123.5, "u0": 0.1, "tE": -5.0}
|
|
585
|
+
>>> messages = validate_solution_consistency("1S1L", params)
|
|
586
|
+
>>> print(messages)
|
|
587
|
+
["Einstein crossing time (tE) must be positive"]
|
|
588
|
+
|
|
589
|
+
>>> # Invalid mass ratio
|
|
590
|
+
>>> params = {"t0": 2459123.5, "u0": 0.1, "tE": 20.0, "q": 1.5}
|
|
591
|
+
>>> messages = validate_solution_consistency("1S2L", params)
|
|
592
|
+
>>> print(messages)
|
|
593
|
+
["Mass ratio (q) should be between 0 and 1"]
|
|
594
|
+
|
|
595
|
+
>>> # Invalid relative probability
|
|
596
|
+
>>> messages = validate_solution_consistency("1S1L", params, relative_probability=1.5)
|
|
597
|
+
>>> print(messages)
|
|
598
|
+
["Relative probability should be between 0 and 1"]
|
|
599
|
+
|
|
600
|
+
>>> # Binary lens with unusual separation
|
|
601
|
+
>>> params = {"t0": 2459123.5, "u0": 0.1, "tE": 20.0, "s": 0.1, "q": 0.5}
|
|
602
|
+
>>> messages = validate_solution_consistency("1S2L", params)
|
|
603
|
+
>>> print(messages)
|
|
604
|
+
["Warning: Separation (s) outside typical caustic crossing range (0.5-2.0)"]
|
|
605
|
+
|
|
606
|
+
Note:
|
|
607
|
+
This function focuses on physical consistency rather than statistical
|
|
608
|
+
validation. Warnings are provided for unusual but not impossible
|
|
609
|
+
parameter combinations. The caustic crossing range check for binary
|
|
610
|
+
lenses is a guideline based on typical microlensing events.
|
|
611
|
+
"""
|
|
612
|
+
messages = []
|
|
613
|
+
|
|
614
|
+
# Check for physically impossible values
|
|
615
|
+
if 'tE' in parameters and parameters['tE'] <= 0:
|
|
616
|
+
messages.append("Einstein crossing time (tE) must be positive")
|
|
617
|
+
|
|
618
|
+
if 'q' in parameters and (parameters['q'] <= 0 or parameters['q'] > 1):
|
|
619
|
+
messages.append("Mass ratio (q) should be between 0 and 1")
|
|
620
|
+
|
|
621
|
+
if 's' in parameters and parameters['s'] <= 0:
|
|
622
|
+
messages.append("Separation (s) must be positive")
|
|
623
|
+
|
|
624
|
+
rel_prob = kwargs.get("relative_probability")
|
|
625
|
+
if rel_prob is not None and not 0 <= rel_prob <= 1:
|
|
626
|
+
messages.append("Relative probability should be between 0 and 1")
|
|
627
|
+
|
|
628
|
+
# 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:
|
|
631
|
+
# Check for caustic crossing conditions
|
|
632
|
+
q = parameters['q']
|
|
633
|
+
s = parameters['s']
|
|
634
|
+
|
|
635
|
+
# Simple caustic crossing check
|
|
636
|
+
if s < 0.5 or s > 2.0:
|
|
637
|
+
messages.append("Warning: Separation (s) outside typical caustic crossing range (0.5-2.0)")
|
|
638
|
+
|
|
639
|
+
return messages
|