microlens-submit 0.16.5__py3-none-any.whl → 0.17.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.
@@ -5,7 +5,7 @@ validate, and export a challenge submission using either the Python API or
5
5
  the command line interface.
6
6
  """
7
7
 
8
- __version__ = "0.16.5"
8
+ __version__ = "0.17.0"
9
9
 
10
10
  from .models import Event, Solution, Submission
11
11
  from .utils import load
@@ -187,7 +187,31 @@ def add_solution(
187
187
  physical_param: Optional[List[str]] = typer.Option(
188
188
  None,
189
189
  "--physical-param",
190
- help=("Physical parameters (M_L, D_L, M_planet, a, etc.) " "derived from model parameters [ADVANCED]"),
190
+ help=("Physical parameters (Mtot, D_L, thetaE, etc.) " "derived from model parameters [ADVANCED]"),
191
+ ),
192
+ physical_param_uncertainty: Optional[List[str]] = typer.Option(
193
+ None,
194
+ "--physical-param-uncertainty",
195
+ help=(
196
+ "Physical parameter uncertainties as key=value. "
197
+ "Can be single value (symmetric) or [lower,upper] (asymmetric) [ADVANCED]"
198
+ ),
199
+ ),
200
+ uncertainty_method: Optional[str] = typer.Option(
201
+ None,
202
+ "--uncertainty-method",
203
+ help=(
204
+ "Method used to derive uncertainties [ADVANCED]. "
205
+ "Options: mcmc_posterior, fisher_matrix, bootstrap, propagation, inference, literature, other"
206
+ ),
207
+ ),
208
+ confidence_level: Optional[float] = typer.Option(
209
+ None,
210
+ "--confidence-level",
211
+ help=(
212
+ "Confidence level for uncertainties (default: 0.68 = 1σ) [ADVANCED]. "
213
+ "Standard values: 0.68 (1σ), 0.95 (2σ), 0.997 (3σ)"
214
+ ),
191
215
  ),
192
216
  relative_probability: Optional[float] = typer.Option(
193
217
  None,
@@ -281,6 +305,10 @@ def add_solution(
281
305
  sol.limb_darkening_coeffs = _parse_pairs(limb_darkening_coeff)
282
306
  sol.parameter_uncertainties = _parse_pairs(parameter_uncertainty) or uncertainties
283
307
  sol.physical_parameters = _parse_pairs(physical_param)
308
+ sol.physical_parameter_uncertainties = _parse_pairs(physical_param_uncertainty)
309
+ sol.uncertainty_method = uncertainty_method
310
+ if confidence_level is not None:
311
+ sol.confidence_level = confidence_level
284
312
  sol.log_likelihood = log_likelihood
285
313
  sol.relative_probability = relative_probability
286
314
  sol.n_data_points = n_data_points
@@ -12,6 +12,7 @@ from rich.table import Table
12
12
  from microlens_submit.error_messages import enhance_validation_messages
13
13
  from microlens_submit.text_symbols import symbol
14
14
  from microlens_submit.utils import load
15
+ from microlens_submit.validate_parameters import count_model_parameters
15
16
 
16
17
  console = Console()
17
18
 
@@ -196,13 +197,17 @@ def compare_solutions(
196
197
  need_calc = [s for s in solutions if s.relative_probability is None]
197
198
  if need_calc:
198
199
  can_calc = all(
199
- s.log_likelihood is not None and s.n_data_points and s.n_data_points > 0 and len(s.parameters) > 0
200
+ s.log_likelihood is not None
201
+ and s.n_data_points
202
+ and s.n_data_points > 0
203
+ and count_model_parameters(s.parameters) > 0
200
204
  for s in need_calc
201
205
  )
202
206
  remaining = max(1.0 - provided_sum, 0.0)
203
207
  if can_calc:
204
208
  bic_vals = {
205
- s.solution_id: len(s.parameters) * math.log(s.n_data_points) - 2 * s.log_likelihood
209
+ s.solution_id: count_model_parameters(s.parameters) * math.log(s.n_data_points)
210
+ - 2 * s.log_likelihood
206
211
  for s in need_calc
207
212
  }
208
213
  bic_min = min(bic_vals.values())
@@ -219,7 +224,7 @@ def compare_solutions(
219
224
 
220
225
  rows = []
221
226
  for sol in solutions:
222
- k = len(sol.parameters)
227
+ k = count_model_parameters(sol.parameters)
223
228
  bic = k * math.log(sol.n_data_points) - 2 * sol.log_likelihood
224
229
  rp = sol.relative_probability if sol.relative_probability is not None else rel_prob_map.get(sol.solution_id)
225
230
  rows.append(
@@ -134,7 +134,10 @@ class Solution(BaseModel):
134
134
  limb_darkening_model: Optional[str] = None
135
135
  limb_darkening_coeffs: Optional[dict] = None
136
136
  parameter_uncertainties: Optional[dict] = None
137
+ uncertainty_method: Optional[str] = None
138
+ confidence_level: Optional[float] = 0.68
137
139
  physical_parameters: Optional[dict] = None
140
+ physical_parameter_uncertainties: Optional[dict] = None
138
141
  log_likelihood: Optional[float] = None
139
142
  relative_probability: Optional[float] = None
140
143
  n_data_points: Optional[int] = None
@@ -155,6 +158,7 @@ class Solution(BaseModel):
155
158
  higher_order_effects = values.get("higher_order_effects", [])
156
159
  bands = values.get("bands", [])
157
160
  t_ref = values.get("t_ref")
161
+ limb_darkening_coeffs = values.get("limb_darkening_coeffs")
158
162
 
159
163
  # Only check for totally broken objects (e.g., wrong types)
160
164
  basic_errors = []
@@ -176,6 +180,7 @@ class Solution(BaseModel):
176
180
  higher_order_effects=higher_order_effects,
177
181
  bands=bands,
178
182
  t_ref=t_ref,
183
+ limb_darkening_coeffs=limb_darkening_coeffs,
179
184
  )
180
185
  if validation_warnings:
181
186
  warnings.warn(f"Solution created with potential issues: {'; '.join(validation_warnings)}", UserWarning)
@@ -388,6 +393,18 @@ class Solution(BaseModel):
388
393
  )
389
394
  messages.extend(consistency_messages)
390
395
 
396
+ # Check solution metadata (uncertainties, etc.)
397
+ from ..validate_parameters import validate_solution_metadata
398
+
399
+ metadata_messages = validate_solution_metadata(
400
+ parameter_uncertainties=self.parameter_uncertainties,
401
+ physical_parameters=self.physical_parameters,
402
+ physical_parameter_uncertainties=self.physical_parameter_uncertainties,
403
+ uncertainty_method=self.uncertainty_method,
404
+ confidence_level=self.confidence_level,
405
+ )
406
+ messages.extend(metadata_messages)
407
+
391
408
  return messages
392
409
 
393
410
  def _save(self, event_path: Path) -> None:
@@ -18,6 +18,7 @@ import psutil
18
18
  from pydantic import BaseModel, Field
19
19
 
20
20
  from ..text_symbols import symbol
21
+ from ..validate_parameters import count_model_parameters
21
22
  from .event import Event
22
23
  from .solution import Solution
23
24
 
@@ -500,14 +501,15 @@ class Submission(BaseModel):
500
501
  s.log_likelihood is None
501
502
  or s.n_data_points is None
502
503
  or s.n_data_points <= 0
503
- or len(s.parameters) == 0
504
+ or count_model_parameters(s.parameters) == 0
504
505
  ):
505
506
  can_calc = False
506
507
  break
507
508
  remaining = max(1.0 - provided_sum, 0.0)
508
509
  if can_calc:
509
510
  bic_vals = {
510
- s.solution_id: len(s.parameters) * math.log(s.n_data_points) - 2 * s.log_likelihood
511
+ s.solution_id: count_model_parameters(s.parameters) * math.log(s.n_data_points)
512
+ - 2 * s.log_likelihood
511
513
  for s in need_calc
512
514
  }
513
515
  bic_min = min(bic_vals.values())
@@ -38,21 +38,21 @@ Note:
38
38
  from typing import Dict, List, Optional, Set
39
39
 
40
40
  # Tier definitions with their associated event lists
41
+ # --- BEGIN AUTO-GENERATED: TIER_DEFINITIONS ---
41
42
  TIER_DEFINITIONS = {
42
43
  "beginner": {
43
44
  "description": "Beginner challenge tier with limited event set",
44
45
  "event_prefix": "rmdc26_",
45
- "event_range": [0, 200],
46
+ "event_range": [0, 253],
46
47
  },
47
48
  "experienced": {
48
49
  "description": "Experienced challenge tier with full event set",
49
50
  "event_prefix": "rmdc26_",
50
- "event_range": [0, 2000],
51
+ "event_range": [0, 3000],
51
52
  },
52
53
  "test": {
53
54
  "description": "Testing tier for development",
54
55
  "event_list": [
55
- # Add test events here
56
56
  "evt",
57
57
  "test-event",
58
58
  "EVENT001",
@@ -68,17 +68,14 @@ TIER_DEFINITIONS = {
68
68
  "description": "2018 test events tier",
69
69
  "event_prefix": "ulwdc1_",
70
70
  "event_range": [0, 293],
71
- "event_list": [
72
- # Add 2018 test events here
73
- "2018-EVENT-001",
74
- "2018-EVENT-002",
75
- ],
71
+ "event_list": ["2018-EVENT-001", "2018-EVENT-002"],
76
72
  },
77
73
  "None": {
78
74
  "description": "No validation tier (skips event validation)",
79
- "event_list": [], # Empty list means no validation
75
+ "event_list": [],
80
76
  },
81
77
  }
78
+ # --- END AUTO-GENERATED: TIER_DEFINITIONS ---
82
79
 
83
80
  # Cache for event lists to avoid repeated list creation
84
81
  _EVENT_LIST_CACHE: Dict[str, Set[str]] = {}
@@ -60,43 +60,49 @@ Note:
60
60
  import re
61
61
  from typing import Any, Dict, List, Optional
62
62
 
63
+ # --- BEGIN AUTO-GENERATED: MODEL_DEFINITIONS ---
63
64
  MODEL_DEFINITIONS = {
64
- # Single Source, Single Lens (PSPL)
65
65
  "1S1L": {
66
66
  "description": "Point Source, Single Point Lens (standard microlensing)",
67
67
  "required_params_core": ["t0", "u0", "tE"],
68
68
  },
69
- # Single Source, Binary Lens
70
69
  "1S2L": {
71
70
  "description": "Point Source, Binary Point Lens",
72
71
  "required_params_core": ["t0", "u0", "tE", "s", "q", "alpha"],
73
72
  },
74
- # Binary Source, Single Lens
75
73
  "2S1L": {
76
74
  "description": "Binary Source, Single Point Lens",
77
- "required_params_core": ["t0", "u0", "tE"], # Core lens params
75
+ "required_params_core": ["t0", "u0", "tE", "t0_source2", "u0_source2", "flux_ratio"],
78
76
  },
79
- # Other/Unknown model type (allows any parameters)
80
- "other": {
81
- "description": "Other or unknown model type",
82
- "required_params_core": [], # No required parameters for unknown models
83
- },
84
- # Add other model types as needed:
85
- # "2S2L": {
86
- # "description": "Binary Source, Binary Point Lens",
87
- # "required_params_core": ["t0", "u0", "tE", "s", "q", "alpha"]
77
+ # '2S2L': {
78
+ # "description": 'Binary Source, Binary Point Lens',
79
+ # "required_params_core": ['t0', 'u0', 'tE', 's', 'q', 'alpha', 't0_source2', 'u0_source2', 'flux_ratio'],
80
+ # },
81
+ # '1S3L': {
82
+ # "description": 'Point Source, Triple Point Lens',
83
+ # "required_params_core": ['t0', 'u0', 'tE'],
84
+ # },
85
+ # '2S3L': {
86
+ # "description": 'Binary Source, Triple Point Lens',
87
+ # "required_params_core": ['t0', 'u0', 'tE', 't0_source2', 'u0_source2', 'flux_ratio'],
88
88
  # },
89
- # "1S3L": {
90
- # "description": "Point Source, Triple Point Lens",
91
- # "required_params_core": ["t0", "u0", "tE", "s1", "q1", "alpha1", "s2", "q2", "alpha2"]
89
+ # '1S4L': {
90
+ # "description": 'Point Source, Quadruple Point Lens',
91
+ # "required_params_core": ['t0', 'u0', 'tE'],
92
92
  # },
93
- # "2S3L": {
94
- # "description": "Binary Source, Triple Point Lens",
95
- # "required_params_core": ["t0", "u0", "tE", "s1", "q1", "alpha1", "s2", "q2", "alpha2"]
93
+ # '2S4L': {
94
+ # "description": 'Binary Source, Quadruple Point Lens',
95
+ # "required_params_core": ['t0', 'u0', 'tE', 't0_source2', 'u0_source2', 'flux_ratio'],
96
96
  # },
97
+ "other": {
98
+ "description": "Other or unknown model type",
99
+ "required_params_core": [],
100
+ },
97
101
  }
102
+ # --- END AUTO-GENERATED: MODEL_DEFINITIONS ---
98
103
 
99
104
  _FLUX_PARAM_RE = re.compile(r"^F(?P<band>\\d+)_S(?:[12])?$|^F(?P<band_b>\\d+)_B$")
105
+ _LD_PARAM_RE = re.compile(r"^u_(?P<band>\d+)$")
100
106
 
101
107
 
102
108
  def _find_flux_params(parameters: Dict[str, Any]) -> List[str]:
@@ -104,6 +110,11 @@ def _find_flux_params(parameters: Dict[str, Any]) -> List[str]:
104
110
  return [param for param in parameters.keys() if isinstance(param, str) and _FLUX_PARAM_RE.match(param)]
105
111
 
106
112
 
113
+ def _find_ld_params(parameters: Dict[str, Any]) -> List[str]:
114
+ """Return a list of parameters that look like band-specific limb darkening terms."""
115
+ return [param for param in parameters.keys() if isinstance(param, str) and _LD_PARAM_RE.match(param)]
116
+
117
+
107
118
  def _infer_bands_from_flux_params(flux_params: List[str]) -> List[str]:
108
119
  """Infer band identifiers from flux parameter names."""
109
120
  bands = set()
@@ -117,14 +128,94 @@ def _infer_bands_from_flux_params(flux_params: List[str]) -> List[str]:
117
128
  return sorted(bands)
118
129
 
119
130
 
131
+ # Metadata parameters that should NOT be counted in BIC calculations
132
+ # These are solution-level metadata, not model parameters
133
+ METADATA_PARAMETERS = {
134
+ "t_ref", # Reference time (metadata for time-dependent effects)
135
+ "limb_darkening_coeffs", # Fixed LD coefficients (not fitted)
136
+ "limb_darkening_model", # Name of LD model (not a parameter)
137
+ "bands", # List of photometric bands (not a parameter)
138
+ "used_astrometry", # Boolean flag (not a parameter)
139
+ "used_postage_stamps", # Boolean flag (not a parameter)
140
+ }
141
+
142
+ # Physical parameters (derived, not fitted in the microlensing model)
143
+ PHYSICAL_PARAMETERS = {
144
+ "Mtot",
145
+ "M1",
146
+ "M2",
147
+ "M3",
148
+ "M4", # Masses
149
+ "D_L",
150
+ "D_S", # Distances
151
+ "thetaE", # Einstein radius
152
+ "piE",
153
+ "piE_N",
154
+ "piE_E",
155
+ "piE_parallel",
156
+ "piE_perpendicular",
157
+ "piE_l",
158
+ "piE_b", # Parallax
159
+ "mu_rel",
160
+ "mu_rel_N",
161
+ "mu_rel_E",
162
+ "mu_rel_l",
163
+ "mu_rel_b", # Proper motion
164
+ "phi", # Position angle
165
+ }
166
+
167
+
168
+ def count_model_parameters(parameters: Dict[str, Any]) -> int:
169
+ """
170
+ Count the number of actual model parameters for BIC calculation.
171
+
172
+ Excludes:
173
+ - Metadata parameters (t_ref, limb_darkening_coeffs, etc.)
174
+ - Physical parameters (Mtot, D_L, etc.) - these are derived, not fitted
175
+
176
+ Includes:
177
+ - Core model parameters (t0, u0, tE, s, q, alpha, etc.)
178
+ - Higher-order effect parameters (piEN, piEE, rho, etc.)
179
+ - Flux parameters (F0_S, F0_B, etc.)
180
+ - Fitted limb darkening parameters (u_0, u_1, etc.)
181
+
182
+ Args:
183
+ parameters: Dictionary of solution parameters
184
+
185
+ Returns:
186
+ Number of model parameters for BIC calculation
187
+
188
+ Example:
189
+ >>> params = {
190
+ ... 't0': 2459123.5, 'u0': 0.1, 'tE': 20.0,
191
+ ... 'piEN': 0.1, 'piEE': 0.05,
192
+ ... 'F0_S': 1000.0, 'F0_B': 500.0,
193
+ ... 't_ref': 2459123.0, # metadata, not counted
194
+ ... 'Mtot': 0.45, # physical parameter, not counted
195
+ ... }
196
+ >>> count_model_parameters(params)
197
+ 6
198
+ """
199
+ count = 0
200
+ for param in parameters.keys():
201
+ # Skip metadata parameters
202
+ if param in METADATA_PARAMETERS:
203
+ continue
204
+ # Skip physical parameters (derived, not fitted)
205
+ if param in PHYSICAL_PARAMETERS:
206
+ continue
207
+ # Count everything else (core params, higher-order params, flux params, LD params)
208
+ count += 1
209
+ return count
210
+
211
+
212
+ # --- BEGIN AUTO-GENERATED: HIGHER_ORDER_EFFECT_DEFINITIONS ---
120
213
  HIGHER_ORDER_EFFECT_DEFINITIONS = {
121
214
  "parallax": {
122
- "description": "Microlens parallax effect",
123
- "requires_t_ref": True, # A flag to check for the 't_ref' attribute
124
- "required_higher_order_params": [
125
- "piEN",
126
- "piEE",
127
- ], # These are often part of the main parameters if fitted
215
+ "description": "Microlens parallax effect (annual parallax from Earth's orbital motion)",
216
+ "requires_t_ref": True,
217
+ "required_higher_order_params": ["piEN", "piEE"],
218
+ "optional_higher_order_params": ["t_ref"],
128
219
  },
129
220
  "finite-source": {
130
221
  "description": "Finite source size effect",
@@ -132,64 +223,56 @@ HIGHER_ORDER_EFFECT_DEFINITIONS = {
132
223
  "required_higher_order_params": ["rho"],
133
224
  },
134
225
  "lens-orbital-motion": {
135
- "description": "Orbital motion of the lens components",
226
+ "description": "Orbital motion of lens components",
136
227
  "requires_t_ref": True,
137
228
  "required_higher_order_params": ["dsdt", "dadt"],
138
- "optional_higher_order_params": ["dzdt"], # Relative radial rate of change of lenses (if needed)
229
+ "optional_higher_order_params": ["dzdt", "t_ref", "d2sdt2", "d2adt2"],
139
230
  },
140
231
  "xallarap": {
141
- "description": "Source orbital motion (xallarap)",
142
- "requires_t_ref": True, # Xallarap often has a t_ref related to its epoch
143
- "required_higher_order_params": [], # Specific parameters (e.g., orbital period, inclination) to be added here
232
+ "description": "Source orbital motion (xallarap is 'parallax' spelled backwards)",
233
+ "requires_t_ref": True,
234
+ "required_higher_order_params": ["xiEN", "xiEE", "P_xi"],
235
+ "optional_higher_order_params": ["e_xi", "omega_xi", "i_xi"],
144
236
  },
145
237
  "gaussian-process": {
146
238
  "description": "Gaussian process model for time-correlated noise",
147
- "requires_t_ref": False, # GP parameters are usually not time-referenced in this way
148
- "required_higher_order_params": [], # Placeholder for common GP hyperparameters
149
- "optional_higher_order_params": [
150
- "ln_K",
151
- "ln_lambda",
152
- "ln_period",
153
- "ln_gamma",
154
- ], # Common GP params, or specific names like "amplitude", "timescale", "periodicity" etc.
239
+ "requires_t_ref": False,
240
+ "required_higher_order_params": [],
241
+ "optional_higher_order_params": ["ln_K", "ln_lambda", "ln_period", "ln_gamma", "ln_a", "ln_c"],
155
242
  },
156
243
  "stellar-rotation": {
157
244
  "description": "Effect of stellar rotation on the light curve (e.g., spots)",
158
- "requires_t_ref": False, # Usually not time-referenced directly in this context
159
- "required_higher_order_params": [], # Specific parameters
160
- # (e.g., rotation period, inclination)
161
- # to be added here
162
- "optional_higher_order_params": [
163
- "v_rot_sin_i",
164
- "epsilon",
165
- ], # Guessing common params: rotational velocity times sin(inclination),
166
- # spot coverage
245
+ "requires_t_ref": False,
246
+ "required_higher_order_params": [],
247
+ "optional_higher_order_params": ["v_rot_sin_i", "epsilon"],
167
248
  },
168
249
  "fitted-limb-darkening": {
169
250
  "description": "Limb darkening coefficients fitted as parameters",
170
251
  "requires_t_ref": False,
171
- "required_higher_order_params": [], # Parameters are usually u1, u2, etc.
172
- # (linear, quadratic)
173
- "optional_higher_order_params": [
174
- "u1",
175
- "u2",
176
- "u3",
177
- "u4",
178
- ], # Common limb darkening coefficients (linear, quadratic, cubic, quartic)
179
- },
180
- # The "other" effect type is handled by allowing any other string in
181
- # `higher_order_effects` list itself.
252
+ "required_higher_order_params": [],
253
+ },
254
+ "other": {
255
+ "description": "Custom higher-order effect",
256
+ "requires_t_ref": False,
257
+ "required_higher_order_params": [],
258
+ },
182
259
  }
260
+ # --- END AUTO-GENERATED: HIGHER_ORDER_EFFECT_DEFINITIONS ---
183
261
 
184
262
  # This dictionary defines properties/constraints for each known parameter
185
263
  # (e.g., expected type, units, a more detailed description, corresponding
186
264
  # uncertainty field name)
265
+ # --- BEGIN AUTO-GENERATED: PARAMETER_PROPERTIES ---
187
266
  PARAMETER_PROPERTIES = {
188
267
  # Core Microlensing Parameters
189
- "t0": {"type": "float", "units": "HJD", "description": "Time of closest approach"},
268
+ "t0": {
269
+ "type": "float",
270
+ "units": "HJD",
271
+ "description": "Time of closest approach",
272
+ },
190
273
  "u0": {
191
274
  "type": "float",
192
- "units": "thetaE",
275
+ "units": "θE",
193
276
  "description": "Minimum impact parameter",
194
277
  },
195
278
  "tE": {
@@ -197,165 +280,248 @@ PARAMETER_PROPERTIES = {
197
280
  "units": "days",
198
281
  "description": "Einstein radius crossing time",
199
282
  },
283
+ # Binary Lens Parameters
200
284
  "s": {
201
285
  "type": "float",
202
- "units": "thetaE",
286
+ "units": "θE",
203
287
  "description": "Binary separation scaled by Einstein radius",
204
288
  },
205
- "q": {"type": "float", "units": "mass ratio", "description": "Mass ratio M2/M1"},
289
+ "q": {
290
+ "type": "float",
291
+ "units": "dimensionless",
292
+ "description": "Mass ratio M2/M1",
293
+ },
206
294
  "alpha": {
207
295
  "type": "float",
208
296
  "units": "rad",
209
297
  "description": "Angle of source trajectory relative to binary axis",
210
298
  },
211
- # Higher-Order Effect Parameters
299
+ # Binary_Source
300
+ "t0_source2": {
301
+ "type": "float",
302
+ "units": "BJD",
303
+ "description": "Time of closest approach for second source",
304
+ },
305
+ "u0_source2": {
306
+ "type": "float",
307
+ "units": "θE",
308
+ "description": "Minimum impact parameter for second source",
309
+ },
310
+ "flux_ratio": {
311
+ "type": "float",
312
+ "units": "dimensionless",
313
+ "description": "Flux ratio of second source to first source",
314
+ },
315
+ # Finite Source Parameters
212
316
  "rho": {
213
317
  "type": "float",
214
- "units": "thetaE",
215
- "description": "Source radius scaled by Einstein radius (Finite Source)",
318
+ "units": "θE",
319
+ "description": "Source radius scaled by Einstein radius",
216
320
  },
321
+ # Parallax Parameters
217
322
  "piEN": {
218
323
  "type": "float",
219
- "units": "Einstein radius",
220
- "description": "Parallax vector component (North) (Parallax)",
324
+ "units": "θE",
325
+ "description": "Parallax vector component (North)",
221
326
  },
222
327
  "piEE": {
223
328
  "type": "float",
224
- "units": "Einstein radius",
225
- "description": "Parallax vector component (East) (Parallax)",
329
+ "units": "θE",
330
+ "description": "Parallax vector component (East)",
226
331
  },
332
+ # Lens Orbital Motion Parameters
227
333
  "dsdt": {
228
334
  "type": "float",
229
- "units": "thetaE/year",
230
- "description": "Rate of change of binary separation (Lens Orbital Motion)",
335
+ "units": "θE/year",
336
+ "description": "Rate of change of binary separation",
231
337
  },
232
338
  "dadt": {
233
339
  "type": "float",
234
340
  "units": "rad/year",
235
- "description": "Rate of change of binary angle (Lens Orbital Motion)",
341
+ "description": "Rate of change of binary orientation",
236
342
  },
237
343
  "dzdt": {
238
344
  "type": "float",
239
345
  "units": "au/year",
240
- "description": "Relative radial rate of change of lenses (Lens Orbital Motion, if applicable)",
241
- }, # Example, may vary
242
- # Flux Parameters (dynamically generated by get_required_flux_params)
243
- # Ensure these names precisely match how they're generated by get_required_flux_params
244
- "F0_S": {
346
+ "description": "Relative radial rate of change of lenses",
347
+ },
348
+ # Xallarap
349
+ "xiEN": {
245
350
  "type": "float",
246
- "units": "counts/s",
247
- "description": "Source flux in band 0",
351
+ "units": "θE",
352
+ "description": "Xallarap vector component (North)",
248
353
  },
249
- "F0_B": {
354
+ "xiEE": {
250
355
  "type": "float",
251
- "units": "counts/s",
252
- "description": "Blend flux in band 0",
356
+ "units": "θE",
357
+ "description": "Xallarap vector component (East)",
253
358
  },
254
- "F1_S": {
359
+ "P_xi": {
255
360
  "type": "float",
256
- "units": "counts/s",
257
- "description": "Source flux in band 1",
361
+ "units": "days",
362
+ "description": "Orbital period of the source companion",
258
363
  },
259
- "F1_B": {
364
+ "e_xi": {
260
365
  "type": "float",
261
- "units": "counts/s",
262
- "description": "Blend flux in band 1",
366
+ "units": "dimensionless",
367
+ "description": "Eccentricity of source orbit",
263
368
  },
264
- "F2_S": {
369
+ "omega_xi": {
265
370
  "type": "float",
266
- "units": "counts/s",
267
- "description": "Source flux in band 2",
371
+ "units": "rad",
372
+ "description": "Argument of periapsis for source orbit",
268
373
  },
269
- "F2_B": {
374
+ "i_xi": {
270
375
  "type": "float",
271
- "units": "counts/s",
272
- "description": "Blend flux in band 2",
376
+ "units": "deg",
377
+ "description": "Inclination of source orbit",
273
378
  },
274
- # Binary Source Flux Parameters (e.g., for "2S" models)
275
- "F0_S1": {
379
+ # Gaussian Process Parameters
380
+ "ln_K": {
276
381
  "type": "float",
277
- "units": "counts/s",
278
- "description": "Primary source flux in band 0",
382
+ "units": "mag²",
383
+ "description": "Log-amplitude of the GP kernel",
279
384
  },
280
- "F0_S2": {
385
+ "ln_lambda": {
281
386
  "type": "float",
282
- "units": "counts/s",
283
- "description": "Secondary source flux in band 0",
387
+ "units": "days",
388
+ "description": "Log-lengthscale of the GP kernel",
284
389
  },
285
- "F1_S1": {
390
+ "ln_period": {
286
391
  "type": "float",
287
- "units": "counts/s",
288
- "description": "Primary source flux in band 1",
392
+ "units": "days",
393
+ "description": "Log-period of the GP kernel",
289
394
  },
290
- "F1_S2": {
395
+ "ln_gamma": {
291
396
  "type": "float",
292
- "units": "counts/s",
293
- "description": "Secondary source flux in band 1",
397
+ "units": "dimensionless",
398
+ "description": "Log-smoothing parameter of the GP kernel",
294
399
  },
295
- "F2_S1": {
400
+ # Stellar Rotation Parameters
401
+ "v_rot_sin_i": {
296
402
  "type": "float",
297
- "units": "counts/s",
298
- "description": "Primary source flux in band 2",
403
+ "units": "km/s",
404
+ "description": "Rotational velocity times sin(inclination)",
299
405
  },
300
- "F2_S2": {
406
+ "epsilon": {
301
407
  "type": "float",
302
- "units": "counts/s",
303
- "description": "Secondary source flux in band 2",
408
+ "units": "dimensionless",
409
+ "description": "Spot coverage/brightness parameter",
304
410
  },
305
- # Gaussian Process parameters (examples, often ln-scaled)
306
- "ln_K": {
411
+ # Other
412
+ "flux_parameters": {
307
413
  "type": "float",
308
- "units": "mag^2",
309
- "description": "Log-amplitude of the GP kernel (GP)",
414
+ "units": "",
415
+ "description": "",
310
416
  },
311
- "ln_lambda": {
417
+ # Derived Physical Parameters
418
+ "Mtot": {
312
419
  "type": "float",
313
- "units": "days",
314
- "description": "Log-lengthscale of the GP kernel (GP)",
420
+ "units": "M_sun",
421
+ "description": "Total lens mass",
315
422
  },
316
- "ln_period": {
423
+ "M1": {
317
424
  "type": "float",
318
- "units": "days",
319
- "description": "Log-period of the GP kernel (GP)",
425
+ "units": "M_sun",
426
+ "description": "Primary lens mass",
320
427
  },
321
- "ln_gamma": {
428
+ "M2": {
322
429
  "type": "float",
323
- "units": " ",
324
- "description": "Log-smoothing parameter of the GP kernel (GP)",
325
- }, # Specific interpretation varies by kernel
326
- # Stellar Rotation parameters (examples)
327
- "v_rot_sin_i": {
430
+ "units": "M_sun",
431
+ "description": "Secondary lens mass",
432
+ },
433
+ "M3": {
328
434
  "type": "float",
329
- "units": "km/s",
330
- "description": "Rotational velocity times sin(inclination) (Stellar Rotation)",
435
+ "units": "M_sun",
436
+ "description": "Tertiary lens mass",
331
437
  },
332
- "epsilon": {
438
+ "M4": {
439
+ "type": "float",
440
+ "units": "M_sun",
441
+ "description": "Quaternary lens mass",
442
+ },
443
+ "D_L": {
444
+ "type": "float",
445
+ "units": "kpc",
446
+ "description": "Lens distance from observer",
447
+ },
448
+ "D_S": {
449
+ "type": "float",
450
+ "units": "kpc",
451
+ "description": "Source distance from observer",
452
+ },
453
+ "thetaE": {
454
+ "type": "float",
455
+ "units": "mas",
456
+ "description": "Angular Einstein radius",
457
+ },
458
+ "piE": {
459
+ "type": "float",
460
+ "units": "dimensionless",
461
+ "description": "Microlens parallax magnitude",
462
+ },
463
+ "piE_N": {
464
+ "type": "float",
465
+ "units": "dimensionless",
466
+ "description": "North component of microlens parallax vector",
467
+ },
468
+ "piE_E": {
469
+ "type": "float",
470
+ "units": "dimensionless",
471
+ "description": "East component of microlens parallax vector",
472
+ },
473
+ "piE_parallel": {
474
+ "type": "float",
475
+ "units": "dimensionless",
476
+ "description": "Component of parallax vector parallel to lens-source relative motion",
477
+ },
478
+ "piE_perpendicular": {
479
+ "type": "float",
480
+ "units": "dimensionless",
481
+ "description": "Component of parallax vector perpendicular to lens-source relative motion",
482
+ },
483
+ "piE_l": {
484
+ "type": "float",
485
+ "units": "dimensionless",
486
+ "description": "Galactic longitude component of microlens parallax vector",
487
+ },
488
+ "piE_b": {
489
+ "type": "float",
490
+ "units": "dimensionless",
491
+ "description": "Galactic latitude component of microlens parallax vector",
492
+ },
493
+ "mu_rel": {
333
494
  "type": "float",
334
- "units": " ",
335
- "description": "Spot coverage/brightness parameter (Stellar Rotation)",
336
- }, # Example, may vary
337
- # Fitted Limb Darkening coefficients (examples)
338
- "u1": {
495
+ "units": "mas/yr",
496
+ "description": "Magnitude of the relative proper motion between lens and source",
497
+ },
498
+ "mu_rel_N": {
339
499
  "type": "float",
340
- "units": " ",
341
- "description": "Linear limb darkening coefficient (Fitted Limb Darkening)",
500
+ "units": "mas/yr",
501
+ "description": "North component of the relative proper motion between lens and source",
342
502
  },
343
- "u2": {
503
+ "mu_rel_E": {
344
504
  "type": "float",
345
- "units": " ",
346
- "description": "Quadratic limb darkening coefficient (Fitted Limb Darkening)",
505
+ "units": "mas/yr",
506
+ "description": "East component of the relative proper motion between lens and source",
347
507
  },
348
- "u3": {
508
+ "mu_rel_l": {
349
509
  "type": "float",
350
- "units": " ",
351
- "description": "Cubic limb darkening coefficient (Fitted Limb Darkening)",
510
+ "units": "mas/yr",
511
+ "description": "Galactic longitude component of the relative proper motion between lens and source",
352
512
  },
353
- "u4": {
513
+ "mu_rel_b": {
354
514
  "type": "float",
355
- "units": " ",
356
- "description": "Quartic limb darkening coefficient (Fitted Limb Darkening)",
515
+ "units": "mas/yr",
516
+ "description": "Galactic latitude component of the relative proper motion between lens and source",
517
+ },
518
+ "phi": {
519
+ "type": "float",
520
+ "units": "rad",
521
+ "description": "Angle of lens-source relative proper motion relative to ecliptic",
357
522
  },
358
523
  }
524
+ # --- END AUTO-GENERATED: PARAMETER_PROPERTIES ---
359
525
 
360
526
 
361
527
  def get_required_flux_params(model_type: str, bands: List[str]) -> List[str]:
@@ -536,12 +702,23 @@ def check_solution_completeness(
536
702
  f"{example_bands!r})."
537
703
  )
538
704
 
705
+ ld_params = _find_ld_params(parameters)
706
+ if ld_params and not bands:
707
+ messages.append("Limb darkening parameters (u_{band}) provided but bands is empty.")
708
+
539
709
  if bands:
540
710
  required_flux_params = get_required_flux_params(model_type, bands)
541
711
  for param in required_flux_params:
542
712
  if param not in parameters:
543
713
  messages.append(f"Missing required flux parameter '{param}' for bands " f"{bands}")
544
714
 
715
+ # If fitted-limb-darkening is active, check for LD params
716
+ if higher_order_effects and "fitted-limb-darkening" in higher_order_effects:
717
+ for band in bands:
718
+ expected_ld = f"u_{band}"
719
+ if expected_ld not in parameters:
720
+ messages.append(f"Missing required limb darkening parameter '{expected_ld}' for band '{band}'")
721
+
545
722
  # Check for invalid parameters (not in any definition)
546
723
  all_valid_params = set()
547
724
 
@@ -560,12 +737,23 @@ def check_solution_completeness(
560
737
  if bands:
561
738
  all_valid_params.update(get_required_flux_params(model_type, bands))
562
739
 
740
+ # Add allowed LD params if effect is active
741
+ if higher_order_effects and "fitted-limb-darkening" in higher_order_effects:
742
+ for band in bands:
743
+ all_valid_params.add(f"u_{band}")
744
+
563
745
  # Check for invalid parameters
564
746
  invalid_params = set(parameters.keys()) - all_valid_params
565
747
  if flux_params and not bands:
566
748
  invalid_params -= set(flux_params)
749
+
750
+ # Allow LD params if fitted-limb-darkening is active
751
+ # (even if not strictly in all_valid_params above if bands missing)
752
+ if higher_order_effects and "fitted-limb-darkening" in higher_order_effects:
753
+ invalid_params -= set(ld_params)
754
+
567
755
  for param in invalid_params:
568
- messages.append(f"Warning: Parameter '{param}' not recognized for model type " f"'{model_type}'")
756
+ messages.append(f"Warning: Parameter '{param}' not recognized for model type '{model_type}'")
569
757
 
570
758
  return messages
571
759
 
@@ -576,43 +764,7 @@ def validate_parameter_types(
576
764
  ) -> List[str]:
577
765
  """Validate parameter types and value ranges against expected types.
578
766
 
579
- Checks that parameters have the correct data types as defined in
580
- PARAMETER_PROPERTIES. Currently supports validation of float, int,
581
- and string types. Parameters not defined in PARAMETER_PROPERTIES
582
- are skipped (no validation performed).
583
-
584
- Args:
585
- parameters: Dictionary of model parameters with parameter names as keys.
586
- model_type: The type of microlensing model (used for context in messages).
587
-
588
- Returns:
589
- List of validation messages. Empty list if all validations pass.
590
- Messages indicate type mismatches for known parameters.
591
-
592
- Example:
593
- >>> # Valid parameters
594
- >>> params = {"t0": 2459123.5, "u0": 0.1, "tE": 20.0}
595
- >>> messages = validate_parameter_types(params, "1S1L")
596
- >>> print(messages)
597
- []
598
-
599
- >>> # Invalid type for t0
600
- >>> params = {"t0": "2459123.5", "u0": 0.1, "tE": 20.0} # t0 is string
601
- >>> messages = validate_parameter_types(params, "1S1L")
602
- >>> print(messages)
603
- ["Parameter 't0' should be numeric, got str"]
604
-
605
- >>> # Unknown parameter (no validation performed)
606
- >>> params = {"t0": 2459123.5, "custom_param": "value"}
607
- >>> messages = validate_parameter_types(params, "1S1L")
608
- >>> print(messages)
609
- []
610
-
611
- Note:
612
- This function only validates parameters that are defined in
613
- PARAMETER_PROPERTIES. Unknown parameters are ignored to allow
614
- for custom parameters and future extensions. The validation
615
- is currently limited to basic type checking (float, int, str).
767
+ ... (omitted docstring for brevity) ...
616
768
  """
617
769
  messages = []
618
770
 
@@ -622,15 +774,17 @@ def validate_parameter_types(
622
774
  for param, value in parameters.items():
623
775
  if param in PARAMETER_PROPERTIES:
624
776
  prop = PARAMETER_PROPERTIES[param]
625
-
626
777
  # Check type
627
778
  expected_type = prop.get("type")
628
779
  if expected_type == "float" and not isinstance(value, (int, float)):
629
- messages.append(f"Parameter '{param}' should be numeric, got " f"{type(value).__name__}")
780
+ messages.append(f"Parameter '{param}' should be numeric, got {type(value).__name__}")
630
781
  elif expected_type == "int" and not isinstance(value, int):
631
- messages.append(f"Parameter '{param}' should be integer, got " f"{type(value).__name__}")
782
+ messages.append(f"Parameter '{param}' should be integer, got {type(value).__name__}")
632
783
  elif expected_type == "str" and not isinstance(value, str):
633
- messages.append(f"Parameter '{param}' should be string, got " f"{type(value).__name__}")
784
+ messages.append(f"Parameter '{param}' should be string, got {type(value).__name__}")
785
+ elif _LD_PARAM_RE.match(param):
786
+ if not isinstance(value, (int, float)):
787
+ messages.append(f"Parameter '{param}' should be numeric, got {type(value).__name__}")
634
788
 
635
789
  return messages
636
790
 
@@ -844,6 +998,123 @@ def validate_solution_consistency(
844
998
  if s < 0.5 or s > 2.0:
845
999
  messages.append("Warning: " "Separation (s) outside typical caustic crossing range " "(0.5-2.0)")
846
1000
 
1001
+ # Run physical parameter validation
1002
+ messages.extend(validate_physical_parameters(parameters))
1003
+
1004
+ return messages
1005
+
1006
+
1007
+ def validate_uncertainty_metadata(
1008
+ parameter_uncertainties: Optional[Dict[str, Any]],
1009
+ physical_parameter_uncertainties: Optional[Dict[str, Any]],
1010
+ uncertainty_method: Optional[str],
1011
+ confidence_level: Optional[float],
1012
+ ) -> List[str]:
1013
+ """Validate uncertainty metadata for completeness and consistency.
1014
+
1015
+ Provides recommendations (not requirements) for uncertainty reporting.
1016
+ """
1017
+ warnings = []
1018
+
1019
+ # Check if uncertainties are provided without metadata
1020
+ has_param_unc = parameter_uncertainties is not None and len(parameter_uncertainties) > 0
1021
+ has_phys_unc = physical_parameter_uncertainties is not None and len(physical_parameter_uncertainties) > 0
1022
+
1023
+ if (has_param_unc or has_phys_unc) and not uncertainty_method:
1024
+ warnings.append(
1025
+ "Recommendation: Uncertainties provided without uncertainty_method. "
1026
+ "Consider adding --uncertainty-method to improve evaluation "
1027
+ "(options: mcmc_posterior, fisher_matrix, bootstrap, propagation, inference, literature, other)"
1028
+ )
1029
+
1030
+ # Validate confidence level if provided
1031
+ if confidence_level is not None:
1032
+ if not 0 < confidence_level < 1:
1033
+ warnings.append(f"Confidence level ({confidence_level}) should be between 0 and 1")
1034
+ elif confidence_level not in [0.68, 0.95, 0.997]:
1035
+ warnings.append(
1036
+ f"Unusual confidence_level: {confidence_level}. " "Standard values are 0.68 (1σ), 0.95 (2σ), 0.997 (3σ)"
1037
+ )
1038
+
1039
+ # Validate uncertainty method if provided
1040
+ valid_methods = ["mcmc_posterior", "fisher_matrix", "bootstrap", "propagation", "inference", "literature", "other"]
1041
+ if uncertainty_method and uncertainty_method not in valid_methods:
1042
+ warnings.append(
1043
+ f"Unknown uncertainty_method: '{uncertainty_method}'. " f"Valid options: {', '.join(valid_methods)}"
1044
+ )
1045
+
1046
+ # Recommend physical parameter uncertainties if physical parameters exist
1047
+ # (This is handled in validate_physical_parameters now)
1048
+
1049
+ return warnings
1050
+
1051
+
1052
+ def validate_physical_parameters(parameters: Dict[str, Any]) -> List[str]:
1053
+ """Validate physical parameters for consistency and range.
1054
+
1055
+ Checks:
1056
+ - Mass consistency (Mtot vs sum of components)
1057
+ - Vector magnitude consistency
1058
+ - Distance limits and ordering (D_L < D_S)
1059
+ - Reasonable mass ranges (warn on unit confusion)
1060
+ """
1061
+ messages = []
1062
+
1063
+ # 1. Mass Consistency
1064
+ # Check if Mtot matches sum of components (M1, M2, M3, M4)
1065
+ mass_components = []
1066
+ for k in ["M1", "M2", "M3", "M4"]:
1067
+ if k in parameters:
1068
+ mass_components.append(parameters[k])
1069
+
1070
+ if "Mtot" in parameters and mass_components:
1071
+ total_comp = sum(mass_components)
1072
+ # Allow 1% error or 1e-6 absolute
1073
+ if abs(parameters["Mtot"] - total_comp) > max(parameters["Mtot"] * 0.01, 1e-6):
1074
+ messages.append(f"Total mass Mtot ({parameters['Mtot']}) does not match sum of components ({total_comp})")
1075
+
1076
+ # 2. Vector Consistency
1077
+ # piE magnitude vs components (piE_N, piE_E)
1078
+ if "piE" in parameters and "piE_N" in parameters and "piE_E" in parameters:
1079
+ mag = (parameters["piE_N"] ** 2 + parameters["piE_E"] ** 2) ** 0.5
1080
+ if abs(parameters["piE"] - mag) > max(parameters["piE"] * 0.01, 1e-6):
1081
+ messages.append(
1082
+ f"piE magnitude ({parameters['piE']}) inconsistent with N/E components (calculated {mag:.4f})"
1083
+ )
1084
+
1085
+ # mu_rel magnitude vs components (mu_rel_N, mu_rel_E)
1086
+ if "mu_rel" in parameters and "mu_rel_N" in parameters and "mu_rel_E" in parameters:
1087
+ mag = (parameters["mu_rel_N"] ** 2 + parameters["mu_rel_E"] ** 2) ** 0.5
1088
+ if abs(parameters["mu_rel"] - mag) > max(parameters["mu_rel"] * 0.01, 1e-6):
1089
+ messages.append(
1090
+ f"mu_rel magnitude ({parameters['mu_rel']}) inconsistent with N/E components (calculated {mag:.4f})"
1091
+ )
1092
+
1093
+ # 3. Distance Checks
1094
+ if "D_L" in parameters and parameters["D_L"] > 25.0:
1095
+ messages.append(f"Warning: Lens distance D_L ({parameters['D_L']} kpc) is unusually large (> 25 kpc)")
1096
+
1097
+ if "D_S" in parameters and parameters["D_S"] > 25.0:
1098
+ messages.append(f"Warning: Source distance D_S ({parameters['D_S']} kpc) is unusually large (> 25 kpc)")
1099
+
1100
+ if "D_L" in parameters and "D_S" in parameters:
1101
+ if parameters["D_L"] >= parameters["D_S"]:
1102
+ messages.append(
1103
+ f"Lens distance D_L ({parameters['D_L']}) must be smaller "
1104
+ f"than source distance D_S ({parameters['D_S']})"
1105
+ )
1106
+
1107
+ # 4. Mass Magnitude Warnings
1108
+ # Warn if any mass component is > 20 M_sun (possible unit confusion with Jupiter masses or unreasonable value)
1109
+ # Jupiter mass is ~0.001 Solar Mass (so 1 M_J ~ 0.001 M_S).
1110
+ # If they enter '1' meaning M_J, they get 1 M_S (reasonable).
1111
+ # If they enter '1000' meaning M_J (approx 1 M_S), they get 1000 M_S (unreasonable).
1112
+ for m_key in ["Mtot", "M1", "M2", "M3", "M4"]:
1113
+ if m_key in parameters and parameters[m_key] > 20.0:
1114
+ messages.append(
1115
+ f"Warning: {m_key} ({parameters[m_key]} M_sun) is very large. Check units (should be Solar masses)."
1116
+ )
1117
+
847
1118
  return messages
848
1119
 
849
1120
 
@@ -853,6 +1124,7 @@ def validate_solution_rigorously(
853
1124
  higher_order_effects: Optional[List[str]] = None,
854
1125
  bands: Optional[List[str]] = None,
855
1126
  t_ref: Optional[float] = None,
1127
+ limb_darkening_coeffs: Optional[Dict[str, List[float]]] = None,
856
1128
  ) -> List[str]:
857
1129
  """Extremely rigorous validation of solution parameters.
858
1130
 
@@ -863,6 +1135,7 @@ def validate_solution_rigorously(
863
1135
  - bands must be a list of strings
864
1136
  - All required flux parameters must be present for each band
865
1137
  - Only "other" model types or effects can have unknown parameters
1138
+ - If limb_darkening_coeffs is provided, it must match bands
866
1139
 
867
1140
  Args:
868
1141
  model_type: The type of microlensing model
@@ -870,6 +1143,7 @@ def validate_solution_rigorously(
870
1143
  higher_order_effects: List of higher-order effects
871
1144
  bands: List of photometric bands
872
1145
  t_ref: Reference time for time-dependent effects
1146
+ limb_darkening_coeffs: Dictionary of fixed limb darkening coefficients
873
1147
 
874
1148
  Returns:
875
1149
  List of validation error messages. Empty list if all validations pass.
@@ -890,6 +1164,28 @@ def validate_solution_rigorously(
890
1164
  if not isinstance(band, str):
891
1165
  messages.append(f"band {i} must be a string, got {type(band).__name__}")
892
1166
 
1167
+ # Validate limb_darkening_coeffs if provided
1168
+ if limb_darkening_coeffs:
1169
+ if not isinstance(limb_darkening_coeffs, dict):
1170
+ messages.append(f"limb_darkening_coeffs must be a dict, got {type(limb_darkening_coeffs).__name__}")
1171
+ elif bands:
1172
+ # Check coverage of bands if bands are specified
1173
+ missing_ld_bands = [b for b in bands if b not in limb_darkening_coeffs]
1174
+ if missing_ld_bands:
1175
+ messages.append(f"limb_darkening_coeffs missing bands: {missing_ld_bands}")
1176
+
1177
+ # Check for extra bands (warning)
1178
+ extra_ld_bands = [b for b in limb_darkening_coeffs if b not in bands]
1179
+ if extra_ld_bands:
1180
+ messages.append(f"limb_darkening_coeffs contains bands not in solution.bands: {extra_ld_bands}")
1181
+
1182
+ # Validate structure
1183
+ for band, coeffs in limb_darkening_coeffs.items():
1184
+ if not isinstance(coeffs, list):
1185
+ messages.append(f"limb_darkening_coeffs[{band!r}] must be a list of floats")
1186
+ elif not all(isinstance(c, (int, float)) for c in coeffs):
1187
+ messages.append(f"limb_darkening_coeffs[{band!r}] must contain only numeric values")
1188
+
893
1189
  flux_params = _find_flux_params(parameters)
894
1190
  if flux_params and not bands:
895
1191
  inferred_bands = _infer_bands_from_flux_params(flux_params)
@@ -980,4 +1276,40 @@ def validate_solution_rigorously(
980
1276
  if missing_flux:
981
1277
  messages.append(f"Missing required flux parameters for bands {bands}: {missing_flux}")
982
1278
 
1279
+ # 10. Validate physical parameters
1280
+ physical_messages = validate_physical_parameters(parameters)
1281
+ messages.extend(physical_messages)
1282
+
1283
+ return messages
1284
+
1285
+
1286
+ def validate_solution_metadata(
1287
+ parameter_uncertainties: Optional[Dict[str, Any]] = None,
1288
+ physical_parameters: Optional[Dict[str, Any]] = None,
1289
+ physical_parameter_uncertainties: Optional[Dict[str, Any]] = None,
1290
+ uncertainty_method: Optional[str] = None,
1291
+ confidence_level: Optional[float] = None,
1292
+ ) -> List[str]:
1293
+ """Validate solution metadata including uncertainties.
1294
+
1295
+ This is a convenience wrapper that calls all metadata validators.
1296
+ """
1297
+ messages = []
1298
+
1299
+ # Validate uncertainty metadata
1300
+ unc_messages = validate_uncertainty_metadata(
1301
+ parameter_uncertainties,
1302
+ physical_parameter_uncertainties,
1303
+ uncertainty_method,
1304
+ confidence_level,
1305
+ )
1306
+ messages.extend(unc_messages)
1307
+
1308
+ # Recommend physical parameter uncertainties if physical parameters provided
1309
+ if physical_parameters and not physical_parameter_uncertainties:
1310
+ messages.append(
1311
+ "Recommendation: Physical parameters provided without uncertainties. "
1312
+ "Consider adding --physical-param-uncertainty for better evaluation."
1313
+ )
1314
+
983
1315
  return messages
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: microlens-submit
3
- Version: 0.16.5
3
+ Version: 0.17.0
4
4
  Summary: A tool for managing and submitting microlensing solutions
5
5
  Home-page: https://github.com/AmberLee2427/microlens-submit
6
6
  Author: Amber Malpas
@@ -210,15 +210,15 @@ import this file directly.
210
210
 
211
211
  Bibtex:
212
212
  ```
213
- @software{malpas_2025_18246117,
213
+ @software{malpas_2025_18488304,
214
214
  author = {Malpas, Amber},
215
215
  title = {microlens-submit},
216
216
  month = oct,
217
217
  year = 2025,
218
218
  publisher = {Zenodo},
219
219
  version = {v0.16.3},
220
- doi = {10.5281/zenodo.18246117},
221
- url = {https://doi.org/10.5281/zenodo.18246117},
220
+ doi = {10.5281/zenodo.18488304},
221
+ url = {https://doi.org/10.5281/zenodo.18488304},
222
222
  }
223
223
  ```
224
224
 
@@ -1,9 +1,9 @@
1
- microlens_submit/__init__.py,sha256=fyqDCZ_UClGfQ7zkhWJC8HFJqh2RQDudEXw2LwhLIYs,399
1
+ microlens_submit/__init__.py,sha256=uhBoW3tMwuP8KDUYsxCKJ1pLdZ9w-5YodyoLTt7p8bk,399
2
2
  microlens_submit/error_messages.py,sha256=8Wzx1NQiAF7hUOjlt-uGhtPC7akknv_PDkokoxhWquk,10411
3
3
  microlens_submit/text_symbols.py,sha256=PbQSkqF_FwTBs45TkUL0zZl74IYDz7L4xVxy8eKxQsU,3146
4
- microlens_submit/tier_validation.py,sha256=FbVHQwItqQnrCeKhkQSgQmMT6dgreTGOptRRYf-AYVE,6649
4
+ microlens_submit/tier_validation.py,sha256=_t6oRuEx00TLwWxTRMNdcjDJQI9o8E1l8lcJnZC_cKY,6601
5
5
  microlens_submit/utils.py,sha256=0VwCgFOAD4xoNpvY-k15axrb667cXaspzuKrZCGLJ-w,14676
6
- microlens_submit/validate_parameters.py,sha256=lCbLTeCzjZQG00zqZ5yBfDs1PcAzqxuDoJsAQ8W-aFw,39394
6
+ microlens_submit/validate_parameters.py,sha256=3luau3pLIK9IOk03Xcq7egeMu44mzm1vlcJwA7UYAnY,51475
7
7
  microlens_submit/assets/github-desktop_logo.png,sha256=pb4rallKrYQPHt6eC0TmJe_UyyMtf1IrP8_OWK19nH8,479821
8
8
  microlens_submit/assets/rges-pit_logo.png,sha256=45AJypXCymvt3lMeK7MHt1SBhwPpnKCMj6S000Cejtc,537645
9
9
  microlens_submit/cli/__init__.py,sha256=u4ZgVOzUe_gf89FBhY61XWjcfK4oxXCStabYTjBuRRo,82
@@ -13,8 +13,8 @@ microlens_submit/cli/commands/__init__.py,sha256=rzIgY7T2Bz4Lhzts_RiWeoBbMoCuxOD
13
13
  microlens_submit/cli/commands/dossier.py,sha256=6gRJNzUgr29YmYJRcUj9aoiRhjb1r9Uy4dip6z2LaHI,5100
14
14
  microlens_submit/cli/commands/export.py,sha256=ojG2KkOcl90eA0tse6hqy74fHc-YX0o0alzAAbpzJew,8513
15
15
  microlens_submit/cli/commands/init.py,sha256=tpO8YlWZLJmo4PuqQTKHXzvniIyWl7WXxUOURp_yfn4,7425
16
- microlens_submit/cli/commands/solutions.py,sha256=GvbAiiTaCaqcDRHOAReaMY-gDLlahRpYvpnKc2Rajs0,32146
17
- microlens_submit/cli/commands/validation.py,sha256=1M8mYSNopsA30u3S-yFBd3iJZxsDoNa0vhwo9brJ1VQ,9028
16
+ microlens_submit/cli/commands/solutions.py,sha256=XjQtG6V2EA1F-3Aw1G7FtVI9-1LwRkG9Tzry03Yebzg,33248
17
+ microlens_submit/cli/commands/validation.py,sha256=Z6C-2eQzC7mSYqyOTvUY-CtGhsklNnjTxG05iingaos,9225
18
18
  microlens_submit/dossier/__init__.py,sha256=INAacbrY0Wi5ueH8c7b156bGzelyUFcynbE7_YRiku0,1948
19
19
  microlens_submit/dossier/dashboard.py,sha256=4OvTUCxIC4LbAqKwimIFhi65fNo5MMJswiQ5OWtyWFA,19907
20
20
  microlens_submit/dossier/event_page.py,sha256=7740o3tpW9Urv7GSzYdp2TiphvDi6U7XnjlLZYipvLw,14878
@@ -23,11 +23,11 @@ microlens_submit/dossier/solution_page.py,sha256=-5kgkOZ9ziNRNAFpVeT_9-6aCcQRL4v
23
23
  microlens_submit/dossier/utils.py,sha256=-DbWByBMsEeQZ-eUyRT74O_3lakE1vHKUD62jPj1_t4,5839
24
24
  microlens_submit/models/__init__.py,sha256=1sHFjAWyFtGgQBRSo8lBYiPzToo4tIoHP3uBjtgJSPY,861
25
25
  microlens_submit/models/event.py,sha256=ifQqE7d7PJTTI9lGylwWV3EGxgyyNGiJtHbm_DLmuys,17105
26
- microlens_submit/models/solution.py,sha256=ollTpKv8zMSEqIL2Q9gXJTbaX0fWZt6rg76edBmYOWQ,23629
27
- microlens_submit/models/submission.py,sha256=f_ewUFhXghnh-pn077bkfBg_6jVbcN_jRhy2wVdKUgk,27941
28
- microlens_submit-0.16.5.dist-info/licenses/LICENSE,sha256=cy1qkVR-kGxD6FXVsparmU2vHJXYeoyAAHv6SgT67sw,1069
29
- microlens_submit-0.16.5.dist-info/METADATA,sha256=4mSk_0rksMNxU7NOl2dEA7J73Zvb1SwJ-DKGW00WPkM,10673
30
- microlens_submit-0.16.5.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
31
- microlens_submit-0.16.5.dist-info/entry_points.txt,sha256=kA85yhxYrpQnUvVZCRS2giz52gaf1ZOfZFjY4RHIZ2s,62
32
- microlens_submit-0.16.5.dist-info/top_level.txt,sha256=uJ9_bADYRySlhEpP-8vTm90ZLV2SrKEzutAaRx8WF0k,17
33
- microlens_submit-0.16.5.dist-info/RECORD,,
26
+ microlens_submit/models/solution.py,sha256=Mk6cxmWCykWiV6jvZGbLsXIoAxsMY_iAj_3kuXWWLGw,24464
27
+ microlens_submit/models/submission.py,sha256=i16Wd5irnVI9thjrDjXP0QG7-qt-IKSbYMCGDfvwRok,28068
28
+ microlens_submit-0.17.0.dist-info/licenses/LICENSE,sha256=cy1qkVR-kGxD6FXVsparmU2vHJXYeoyAAHv6SgT67sw,1069
29
+ microlens_submit-0.17.0.dist-info/METADATA,sha256=ioshF71XBRmmKyC-wQnEDhzfvAXCuhNVSqnr1QVcMzY,10673
30
+ microlens_submit-0.17.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
31
+ microlens_submit-0.17.0.dist-info/entry_points.txt,sha256=kA85yhxYrpQnUvVZCRS2giz52gaf1ZOfZFjY4RHIZ2s,62
32
+ microlens_submit-0.17.0.dist-info/top_level.txt,sha256=uJ9_bADYRySlhEpP-8vTm90ZLV2SrKEzutAaRx8WF0k,17
33
+ microlens_submit-0.17.0.dist-info/RECORD,,