Add solver settings display and configuration options to rota detail view
This commit is contained in:
@@ -415,6 +415,35 @@ class RotaScheduleForm(forms.ModelForm):
|
||||
else:
|
||||
self.fields[field_name] = forms.CharField(required=False, initial=initial, label=k.replace("_", " "), help_text=meta.get("help"))
|
||||
|
||||
# Expose solver selection and solver options (JSON) so users can tune
|
||||
# solver behaviour per-rota. Persisted into `RotaSchedule.options` as
|
||||
# keys `solver` (string) and `solver_options` (dict).
|
||||
solver_initial = existing_opts.get("solver", "appsi_highs")
|
||||
solver_opts_initial = existing_opts.get("solver_options", {})
|
||||
try:
|
||||
solver_opts_str = json.dumps(solver_opts_initial, indent=2)
|
||||
except Exception:
|
||||
solver_opts_str = ""
|
||||
|
||||
self.fields["solver"] = forms.CharField(required=False, initial=solver_initial, label="Solver", help_text="Solver plugin name to use (e.g. appsi_highs, scip, gurobi)")
|
||||
# Common solver ratio is surfaced as a dedicated field for convenience
|
||||
# (many workflows tune this). It's stored inside the solver_options
|
||||
# dict under the key 'ratio'. The free-form JSON textarea remains for
|
||||
# advanced options.
|
||||
solver_ratio_initial = None
|
||||
try:
|
||||
if isinstance(solver_opts_initial, dict) and "ratio" in solver_opts_initial:
|
||||
solver_ratio_initial = solver_opts_initial.get("ratio")
|
||||
else:
|
||||
# also accept legacy top-level 'ratio' key in existing_opts
|
||||
solver_ratio_initial = existing_opts.get("ratio")
|
||||
except Exception:
|
||||
solver_ratio_initial = None
|
||||
|
||||
self.fields["solver"] = forms.CharField(required=False, initial=solver_initial, label="Solver", help_text="Solver plugin name to use (e.g. appsi_highs, scip, gurobi)")
|
||||
self.fields["solver_ratio"] = forms.FloatField(required=False, initial=solver_ratio_initial, label="Solver ratio (relative gap)", help_text="Relative MIP gap / ratio (e.g. 0.01 for 1%)")
|
||||
self.fields["solver_options"] = forms.CharField(required=False, widget=forms.Textarea(attrs={"rows": 6}), initial=solver_opts_str, label="Solver options (JSON)", help_text="JSON object of solver-specific options; e.g. {\"seconds\": 60, \"threads\": 4}")
|
||||
|
||||
def clean_start_date(self):
|
||||
start = self.cleaned_data.get("start_date")
|
||||
if start is None:
|
||||
@@ -506,6 +535,49 @@ class RotaScheduleForm(forms.ModelForm):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Persist solver selection and options (if provided). Solver options are
|
||||
# stored as a dict under opts['solver_options']. If the user supplied
|
||||
# a JSON string, attempt to parse it; otherwise keep raw value.
|
||||
try:
|
||||
solver_name = self.cleaned_data.get("solver")
|
||||
if solver_name:
|
||||
opts["solver"] = solver_name
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
so = self.cleaned_data.get("solver_options")
|
||||
if so is not None and so != "":
|
||||
if isinstance(so, str):
|
||||
try:
|
||||
parsed = json.loads(so)
|
||||
except Exception:
|
||||
# keep as raw string if parsing fails (non-fatal)
|
||||
parsed = so
|
||||
else:
|
||||
parsed = so
|
||||
opts["solver_options"] = parsed
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# If a dedicated solver_ratio field was provided, inject/override it
|
||||
# into the solver_options dict so it is honoured at runtime. This
|
||||
# ensures the friendly ratio field wins over any value in the JSON
|
||||
# textarea.
|
||||
try:
|
||||
ratio_val = self.cleaned_data.get("solver_ratio")
|
||||
if ratio_val is not None and ratio_val != "":
|
||||
so = opts.get("solver_options")
|
||||
if so is None or not isinstance(so, dict):
|
||||
so = {}
|
||||
try:
|
||||
so["ratio"] = float(ratio_val)
|
||||
except Exception:
|
||||
so["ratio"] = ratio_val
|
||||
opts["solver_options"] = so
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
instance.options = opts
|
||||
|
||||
if commit:
|
||||
|
||||
Reference in New Issue
Block a user