Skip to content

formulation

Render a whole :class:~openscvx.problem.Problem as a Mayer-form formulation.

Where the expression visitors (arithmetic, math, ...) turn a single AST node into LaTeX, this module assembles the problem — objective, dynamics, path constraints, box bounds, and boundary conditions — into one classic optimal-control block::

minimize    <Mayer terms>
subject to  <dynamics>, <path constraints>, <box bounds>, <boundary conditions>

A Mayer form puts the entire objective on the boundary of the trajectory (terms like t(t_f) or x_i(t_0)) rather than as a running integral; this is exactly how OpenSCvx problems are posed, since initial/final boundary types are the only source of cost.

The block renders the pre-augmentation, user-authored problem. The solver augments dynamics with a time-dilation control and per-CTCS penalty states (_-prefixed), but those are machinery, not math the user wrote — so anything named with a leading underscore is filtered out. CTCS constraints render as the continuous-time path constraints they stand for, which is the whole point of the Mayer story.

Each of the two configurable sections has its own detail levels. Dynamics:

  • "inline" — one \dot{<state>} = <expr> row per equation in the formulation.
  • "separate" — the formulation carries \dot{x} = f(x, u) and the per-state definitions follow as their own block.

Constraints add a middle level:

  • "inline" — full constraint bodies with their annotations.
  • "symbolic" — numbered g_i(x, u) \le 0 / h_j(x, u) = 0 references only, \forall t / node annotations kept visible.
  • "separate" — symbolic references with the residual bodies appended as a definition block. The g_i / h_j numbering lines up between the references and the definitions.

The output is a complete display-math fragment: the Mayer form as a subequations/align block (per-row numbering, drops into a paper as-is), with each "separate" definition block a bare align, joined by newlines and no connective text — any where/spacing glue is the author's editorial call. No $$ wrapping. env="aligned" swaps the paper envelope for plain aligned blocks that renderers without subequations support (MathJax, KaTeX) can typeset inside $$.

problem_to_latex(symbolic: SymbolicProblem, dynamics_dict: dict, lam_cost: Union[float, np.ndarray], *, dynamics: str, constraints: str, weights: str = 'symbolic', env: Literal['align', 'aligned'] = 'align') -> str

Assemble a Mayer-form LaTeX formulation of a problem.

Parameters:

Name Type Description Default
symbolic SymbolicProblem

The preprocessed :class:SymbolicProblem (source of states, controls, categorized constraints, and horizon N).

required
dynamics_dict dict

The user-authored {state_name: expr} dynamics, before time/CTCS augmentation.

required
lam_cost Union[float, ndarray]

Objective weights — a scalar or an (n_states,) array indexed by each state's _slice (the resolved algorithm.lam_cost).

required
dynamics str

Detail level for the dynamics section — "inline" or "separate".

required
constraints str

Detail level for the constraint section — "inline", "symbolic", or "separate".

required
weights str

How objective coefficients render — "symbolic" (the default) renders each as a \lambda subscripted by the state's inner symbol and element; "numeric" substitutes the lam_cost values (%g, omitted when equal to 1).

'symbolic'
env Literal['align', 'aligned']

Output envelope. "align" (the default) wraps the Mayer form in subequations + align for paper-grade per-row numbering — the form Problem.to_latex returns. "aligned" emits plain aligned blocks instead; this exists solely for the notebook hook, since MathJax/KaTeX do not implement subequations and align cannot nest inside $$, whereas aligned can.

'align'

Returns:

Type Description
str

One LaTeX display-math fragment: the formulation block followed, in

str

"separate" modes, by a bare definition block per separated section,

str

joined by newlines with no connective glue. No $$ delimiters.

Raises:

Type Description
ValueError

If dynamics, constraints, or weights is not a valid mode.

Source code in openscvx/symbolic/lowerers/latex/formulation.py
def problem_to_latex(
    symbolic: "SymbolicProblem",
    dynamics_dict: dict,
    lam_cost: Union[float, np.ndarray],
    *,
    dynamics: str,
    constraints: str,
    weights: str = "symbolic",
    env: Literal["align", "aligned"] = "align",
) -> str:
    """Assemble a Mayer-form LaTeX formulation of a problem.

    Args:
        symbolic: The preprocessed :class:`SymbolicProblem` (source of states,
            controls, categorized constraints, and horizon ``N``).
        dynamics_dict: The user-authored ``{state_name: expr}`` dynamics, before
            time/CTCS augmentation.
        lam_cost: Objective weights — a scalar or an ``(n_states,)`` array indexed
            by each state's ``_slice`` (the resolved ``algorithm.lam_cost``).
        dynamics: Detail level for the dynamics section — ``"inline"`` or
            ``"separate"``.
        constraints: Detail level for the constraint section — ``"inline"``,
            ``"symbolic"``, or ``"separate"``.
        weights: How objective coefficients render — ``"symbolic"`` (the default)
            renders each as a ``\\lambda`` subscripted by the state's inner symbol
            and element; ``"numeric"`` substitutes the ``lam_cost`` values (``%g``,
            omitted when equal to 1).
        env: Output envelope.  ``"align"`` (the default) wraps the Mayer form in
            ``subequations`` + ``align`` for paper-grade per-row numbering — the
            form ``Problem.to_latex`` returns.  ``"aligned"`` emits plain
            ``aligned`` blocks instead; this exists solely for the notebook hook,
            since MathJax/KaTeX do not implement ``subequations`` and ``align``
            cannot nest inside ``$$``, whereas ``aligned`` can.

    Returns:
        One LaTeX display-math fragment: the formulation block followed, in
        ``"separate"`` modes, by a bare definition block per separated section,
        joined by newlines with no connective glue.  No ``$$`` delimiters.

    Raises:
        ValueError: If ``dynamics``, ``constraints``, or ``weights`` is not a
            valid mode.
    """
    _validate_mode("dynamics", dynamics, _DYNAMICS_MODES)
    _validate_mode("constraints", constraints, _CONSTRAINT_MODES)
    _validate_mode("weights", weights, _WEIGHT_MODES)

    lowerer = LatexLowerer()
    N = symbolic.N

    objective = _objective(symbolic.states, lam_cost, weights, lowerer)

    dyn_pairs = _dynamics_pairs(dynamics_dict, lowerer)
    if dynamics == "inline":
        dyn_rows = [rf"{lhs} = {ex}" for lhs, ex in dyn_pairs]
    else:
        dyn_rows = [r"\dot{x} = f(x, u)"]

    con_refs, con_defs = _constraint_rows(symbolic, N, constraints, lowerer)

    box_rows = _box_bound_rows(list(symbolic.states) + list(symbolic.controls), lowerer)
    bc_rows = _boundary_rows(symbolic.states, lowerer)

    st_rows = dyn_rows + con_refs + box_rows + bc_rows

    blocks = [_formulation(objective, st_rows, env)]
    if dynamics == "separate":
        blocks.append(_definition_block((rf"{lhs} &= {ex}" for lhs, ex in dyn_pairs), env))
    if constraints == "separate":
        blocks.append(
            _definition_block(
                (rf"{label}(x, u) &= {residual}" for label, residual in con_defs), env
            )
        )
    if env == "aligned" and len(blocks) > 1:
        # Adjacent ``aligned`` boxes inside one ``$$`` typeset side by side;
        # ``gathered`` stacks them vertically for the notebook renderer.
        return "\\begin{gathered}\n" + "\n\\\\\n".join(blocks) + "\n\\end{gathered}"
    return "\n".join(blocks)