2025, Dec 27 00:01

Как правильно сочетать CVXPY и NumPy: селектор вместо присваивания

Объясняем как корректно сочетать CVXPY и NumPy: из‑за чего падают типы при присваивании в массивы и как исправить это через матрицу‑селектор и линейную алгебру

При совместном использовании массивов NumPy и CVXPY распространённая ловушка — попытка встроить переменные CVXPY прямо в массив NumPy, а затем применять этот массив в ограничениях и целевых функциях. Снаружи это выглядит естественно, но во время исполнения срывается на ошибках типов и форм. Ниже — краткое, удобное для инженеров пояснение, что именно идёт не так и как перестроить код, чтобы задача оптимизации решалась без сбоев.

Воспроизведение проблемы

Цель — считать NaN неизвестными, оптимизировать эти элементы с ограничением на неотрицательность и задать границы для произведения A @ x. Следующий фрагмент демонстрирует шаблон, который приводит к ошибке.

import cvxpy as cp
import numpy as np

def solve_vector_naive(M, seed_vals):
    seed_vals = np.array(seed_vals, dtype=float)
    miss_mask = np.isnan(seed_vals)
    z_free = cp.Variable(np.sum(miss_mask), nonneg=True)
    x_all = np.copy(seed_vals)
    idx_free = np.where(miss_mask)[0]
    for i, u in enumerate(idx_free):
        x_all[u] = z_free[i]
    y = M @ x_all
    cons = [y >= 1.0, y <= 2.0]
    obj = cp.Minimize(cp.sum(z_free))
    prob = cp.Problem(obj, cons)
    prob.solve()
    if prob.status != cp.OPTIMAL:
        raise ValueError(f"Optimization failed: {prob.status}")
    out = np.copy(x_all)
    for i, u in enumerate(idx_free):
        out[u] = z_free.value[i]
    return out, y.value

Такой подход нередко падает с сообщениями вроде «ValueError: setting an array element with a sequence» и «TypeError: float() argument must be a string or a real number, not 'index'».

Что на самом деле ломается

Массив NumPy с dtype float ожидает конкретные численные значения. Переменная или выражение CVXPY остаётся символическим до решения задачи. Присваивание z_free[i] в ячейку массива NumPy заставляет NumPy преобразовать символическое выражение к float, а это невозможно. В этом и кроется причина упомянутых ValueError и TypeError.

Решение

Вместо вставки переменных CVXPY в массив NumPy постройте матрицу-выборку, которая отображает компактный вектор неизвестных во вектор полной длины. Оставьте известные элементы числовыми, замените их NaN на нули и сформируйте зависящий от решения вектор с помощью линейной алгебры. Так все символические части остаются в графе выражений CVXPY, а численные — в NumPy, без смешивания на уровне отдельных элементов.

import cvxpy as cp
import numpy as np

def solve_vector_with_selector(M, seed_vals):
    seed_vals = np.array(seed_vals, dtype=float)
    miss_mask = np.isnan(seed_vals)
    z_free = cp.Variable(int(np.sum(miss_mask)), nonneg=True)
    x_base = np.copy(seed_vals)
    idx_free = np.where(miss_mask)[0]
    S = np.zeros((len(x_base), z_free.shape[0]))
    for i, u in enumerate(idx_free):
        S[u, i] = 1.0
    x_base[np.isnan(x_base)] = 0.0
    y = M @ (x_base + S @ z_free)
    cons = [y >= 1.0, y <= 2.0]
    obj = cp.Minimize(cp.sum(z_free))
    prob = cp.Problem(obj, cons)
    prob.solve()
    if prob.status != cp.OPTIMAL:
        raise ValueError(f"Optimization failed: {prob.status}")
    x_full_val = x_base + S @ z_free.value
    return np.asarray(x_full_val).ravel(), y.value

Решающее значение имеют два шага. Во‑первых, селектор S «разворачивает» вектор неизвестных z_free до полной размерности, не пытаясь присваивать элементы в NumPy. Во‑вторых, NaN в известном векторе заменяются нулями, поэтому x_base остаётся числовым и совместимым с арифметикой NumPy и CVXPY.

Почему это важно

В задачах оптимизации с ограничениями на CVXPY все символические части должны оставаться в «мире» CVXPY до момента решения. Преждевременный переход границы — запись выражений CVXPY в массивы NumPy — приводит к хрупкому коду и неочевидным ошибкам. Подход с матрицей‑селектором сохраняет чёткое разделение численных данных и символических переменных, делает вычислительный граф явным и удобным для решателя.

Напоследок

Держите переменные CVXPY внутри выражений, а не внутри массивов NumPy. Когда нужно разместить неизвестные в более крупной структуре, используйте линейную алгебру и селектор вместо присваивания по месту. Если что‑то всё же идёт не так, приложите полный стек вызовов и сведите пример к минимально воспроизводимому — так проще увидеть природу сбоя и быстро его устранить.