Source code for cvxlab.frontend.interface

"""Interface loop engine and public entry-point function."""
import inspect
from typing import Any, Dict, List, Optional

from cvxlab.defaults import Defaults
from cvxlab.frontend import actions, display, session
from cvxlab.log_exc.exceptions import CVXLabError


# Parameter groups — each name must match a run() parameter.
# Import-time validation ensures these stay in sync with the
# function signature; a mismatch causes an immediate RuntimeError.

_MODEL_PARAM_NAMES = {
    'model_dir_name', 'main_dir_path', 'model_settings_from',
    'detailed_validation', 'multiple_input_files', 'input_data_files_type',
    'log_level', 'log_format',
}

_SOLVER_PARAM_NAMES = {
    'solver', 'solver_verbose', 'solver_settings',
    'integrated_problems', 'convergence_monitoring', 'convergence_norm',
    'convergence_tables_to_check', 'convergence_tables_to_skip',
    'relative_tolerance', 'maximum_iterations', 'keep_previous_iteration_db',
}

_SESSION_PARAM_NAMES = {'model_structure_file', 'template_file_type'}


def _run_menu(
    menu_actions: List[actions.Action],
    configuration: session.SessionConfig,
    model_state: session.ModelState,
) -> None:
    """Generic menu loop that works for any list of ``Action`` objects.

    Actions whose ``visible`` predicate returns False for the current
    configuration are excluded from the menu.
    Handles sub-menus via recursion when ``action.children`` is set.
    Backend exceptions (``CVXLabError`` and subclasses) are caught so that
    only the package logger output is visible — no raw tracebacks.
    """
    visible = [a for a in menu_actions if a.visible(configuration)]
    labels = [a.label for a in visible]
    n = len(visible)

    while True:
        display.print_menu(labels)
        choice = display.prompt_choice(n)

        if choice.lower() == 'exit':
            display.exit_msg()
            break

        if not choice.isdigit() or not (1 <= int(choice) <= n):
            print(f"\nERROR. Valid selections: 1 to {n}.\n")
            continue

        action = visible[int(choice) - 1]

        display.print_log_start()
        try:
            if action.children:
                _run_menu(action.children, configuration, model_state)
            else:
                action.handler(configuration, model_state)
        except KeyboardInterrupt:
            display.print_log_end()
            print("\nOperation interrupted by user.\n")
        except CVXLabError:
            display.print_log_end()
        except Exception as exc:
            display.print_log_end()
            print(f"\nUnexpected error: {exc}\n")
        else:
            display.print_log_end()

        display.print_separator()


[docs] def run( # Model.__init__ parameters model_dir_name: Optional[str] = None, main_dir_path: Optional[str] = None, model_settings_from: Optional[Defaults.LiteralTypes.SettingsSource] = None, detailed_validation: Optional[bool] = None, multiple_input_files: Optional[bool] = None, input_data_files_type: Optional[Defaults.LiteralTypes.DataFileType] = None, log_level: Optional[Defaults.LiteralTypes.LogLevel] = None, log_format: Optional[Defaults.LiteralTypes.LogFormat] = None, # Model.run_model parameters solver: Optional[str] = None, solver_verbose: Optional[bool] = None, solver_settings: Optional[Dict[str, Any]] = None, integrated_problems: Optional[bool] = None, convergence_monitoring: Optional[bool] = None, convergence_norm: Optional[Defaults.LiteralTypes.NormType] = None, convergence_tables_to_check: Optional[ Defaults.LiteralTypes.ConvergenceTables | List[str]] = None, convergence_tables_to_skip: Optional[List[str]] = None, relative_tolerance: Optional[float] = None, maximum_iterations: Optional[int] = None, keep_previous_iteration_db: Optional[bool] = None, # Frontend-only parameters model_structure_file: Optional[str] = None, template_file_type: Optional[Defaults.LiteralTypes.SettingsSource] = None, ) -> None: """Launch the CVXlab guided user interface. All parameters are optional. When provided, they override the corresponding defaults in ``Model.__init__`` or ``Model.run_model()``. Parameters left as ``None`` are omitted, letting the backend apply its own defaults (or let the user specify them at runtime). Args: model_dir_name (str, optional): The name of the model directory. main_dir_path (str, optional): The main directory path where the model directory is located. If None, the current working directory is used. model_settings_from (Literal['yml', 'xlsx'], optional): The format of the model settings file. Can be either 'yml' or 'xlsx'. detailed_validation (bool, optional): If True, performs detailed validation logging of data and model settings during initialization. multiple_input_files (bool, optional): If True, input data Excel files are generated as one file per data table. If False, all data tables are generated in a single Excel file with multiple tabs. input_data_files_type (Literal['xlsx', 'csv'], optional): The format of the input data files. log_level (Literal['info', 'debug', 'warning', 'error'], optional): The logging level for the logger. log_format (Literal['standard', 'detailed'], optional): The logging format for the logger. solver (str, optional): The solver to use for solving numerical problems. If None, the default solver specified in 'Defaults.NumericalSettings.CVXPY_DEFAULT_SETTINGS' is used. solver_verbose (bool, optional): If True, logs verbose output related to numerical solver operation during the model run. solver_settings (dict[str, Any], optional): Additional settings for the solver passed as key-value pairs. integrated_problems (bool, optional): If True, solve problems iteratively using a block Gauss-Seidel (alternating optimization) scheme, where updated endogenous variables are exchanged until convergence. convergence_monitoring (bool, optional): If True, enables convergence monitoring during the solving of integrated problems. convergence_norm (Literal['max_relative', 'max_absolute', 'l1', 'l2', 'linf'], optional): The norm type to use for convergence monitoring in integrated problems. convergence_tables_to_check (Literal['all_endogenous', 'hybrid_only'] | List[str], optional): The data tables to consider for convergence monitoring in integrated problems. Can be 'all_endogenous', 'hybrid_only', or a list of specific data table keys. convergence_tables_to_skip (List[str], optional): List of data table keys to skip for convergence checking in integrated problems. relative_tolerance (float, optional): Numerical tolerance for verifying maximum relative change between iterations in integrated problems for each data table. Overrides 'Defaults.NumericalSettings.MODEL_COUPLING_SETTINGS'. maximum_iterations (int, optional): The maximum number of iterations for solving integrated problems. Overrides 'Defaults.NumericalSettings.MODEL_COUPLING_SETTINGS'. keep_previous_iteration_db (bool, optional): Whether to keep the database generated during the last-1 iteration. For debugging purpose. model_structure_file (str, optional): Name of the Excel file used to transfer model structure information. template_file_type (Literal['yml', 'xlsx'], optional): The type of template configuration file to generate when creating a model directory. Defaults to 'xlsx'. Example:: import cvxlab cvxlab.run( model_dir_name='my_model', main_dir_path='/path/to/models', log_level='debug', solver='ECOS', ) """ all_args = locals() def _collect_group(names: set[str]) -> dict[str, Any]: return {k: all_args[k] for k in names if all_args[k] is not None} model_kw = _collect_group(_MODEL_PARAM_NAMES) solver_kw = _collect_group(_SOLVER_PARAM_NAMES) session_kw = _collect_group(_SESSION_PARAM_NAMES) cfg = session.SessionConfig( model_kwargs=model_kw, solver_kwargs=solver_kw, **session_kw, ) display.clear_screen() display.print_header() _run_menu( menu_actions=actions.MAIN_MENU, configuration=cfg, model_state=session.ModelState() )
# Import-time validation: param groups ↔ run() signature def _validate_param_groups() -> None: sig_params = set(inspect.signature(run).parameters) grouped = _MODEL_PARAM_NAMES | _SOLVER_PARAM_NAMES | _SESSION_PARAM_NAMES missing_from_groups = sig_params - grouped extra_in_groups = grouped - sig_params if missing_from_groups or extra_in_groups: raise RuntimeError( f"run() parameter groups out of sync — " f"missing from groups: {missing_from_groups or '{}'}, " f"extra in groups: {extra_in_groups or '{}'}" ) # Run validation at import time so that any mismatch causes an immediate error. _validate_param_groups()