Лучший способ построения функции с памятью

Добрый день,

У меня есть очень слабое и сложное функционирование, скажем f[x,y]. И мне нужно построить подробный ContourPlot. Кроме того, функция f[x,y] иногда терпит неудачу из-за отсутствия физической памяти. В таких случаях я должен прекратить оценку и самостоятельно исследовать вопрос о задаче точки {x, y}. Затем я должен добавить элемент {x, y, f [x, y]} в список вычисленных значений f[x,y] (скажем, "кеш" ) и перезапустить оценку ContourPlot. ContourPlot должен принимать все уже вычисленные значения f из кеша. Я бы предпочел сохранить такой список в каком-то файле, чтобы иметь возможность повторно использовать его позже. И, вероятно, проще добавить проблематичные точки в этот файл вручную.

Каков самый быстрый способ реализовать это, если список вычисленных значений f может содержать 10000-50000 точек?

Ответ 1

Предположим, что наша медленная функция имеет подпись f[x, y].

Чистый подход в памяти

Если вас устраивает кеш в памяти, проще всего использовать memoization:

[email protected]
fmem[x_, y_] := fmem[x, y] = f[x, y]

Это добавляет определение к себе каждый раз, когда он вызывается с комбинацией аргументов, которые он не видел раньше.

Подход, основанный на файлах в памяти

Однако, если у вас заканчивается память или возникают сбои ядра во время длинных вычислений, вы захотите вернуть этот кеш с некоторой настойчивостью. Самое простое - сохранить исполняемый файл журнала:

$runningLogFile = "/some/directory/runningLog.txt";

[email protected]
flog[x_, y_] := flog[x, y] = f[x, y] /.
  v_ :> (PutAppend[Unevaluated[flog[x, y] = v;], $runningLogFile]; v)

If[FileExistsQ[$runningLogFile]
, Get[$runningLogFile]
, Export[$runningLogFile, "", "Text"];
]

flog совпадает с fmem, за исключением того, что он также записывает запись в текущий журнал, который может быть использован для восстановления кэшированного определения в более позднем сеансе. Последнее выражение перезагружает эти определения, когда находит существующий файл журнала (или создает файл, если он не существует).

Текстовый характер файла журнала удобен, когда требуется ручное вмешательство. Имейте в виду, что текстовое представление чисел с плавающей запятой вводит неизбежные ошибки округления, поэтому вы можете получить несколько разные результаты после перезагрузки значений из файла журнала. Если это вызывает большую озабоченность, вы можете рассмотреть возможность использования двоичной функции DumpSave, хотя я оставлю детали этого подхода читателю, так как это не совсем удобно для ведения инкрементного журнала.

Подход SQL

Если память действительно жесткая, и вы хотите избежать большого кэша в памяти, чтобы освободить место для других вычислений, предыдущая стратегия может оказаться неприемлемой. В этом случае вы можете использовать встроенную базу данных SQL Mathematica для хранения кэша полностью извне:

fsql[x_, y_] :=
  loadCachedValue[x, y] /. $Failed :> saveCachedValue[x, y, f[x, y]]

Я определяю loadCachedValue и saveCachedValue ниже. Основная идея - создать таблицу SQL, в которой каждая строка содержит тройку x, y, f. Таблица SQL запрашивается каждый раз, когда требуется значение. Обратите внимание, что этот подход существенно медленнее, чем кеш в памяти, поэтому он имеет наибольший смысл, когда вычисление f занимает гораздо больше времени, чем время доступа SQL. SQL-подход не страдает от ошибок округления, которые нарушают подход текстового лог файла.

Далее следуют определения loadCachedValue и saveCachedValue, а также некоторые другие полезные вспомогательные функции:

Needs["DatabaseLink`"]

$cacheFile = "/some/directory/cache.hsqldb";

openCacheConnection[] :=
  $cache = OpenSQLConnection[JDBC["HSQL(Standalone)", $cacheFile]]

closeCacheConnection[] :=
  CloseSQLConnection[$cache]

createCache[] :=
  SQLExecute[$cache,
    "CREATE TABLE cached_values (x float, y float, f float)
     ALTER TABLE cached_values ADD CONSTRAINT pk_cached_values PRIMARY KEY (x, y)"
  ]

saveCachedValue[x_, y_, value_] :=
  ( SQLExecute[$cache,
      "INSERT INTO cached_values (x, y, f) VALUES (?, ?, ?)", {x, y, value}
    ]
  ; value
  )

loadCachedValue[x_, y_] :=
  SQLExecute[$cache,
    "SELECT f FROM cached_values WHERE x = ? AND y = ?", {x, y}
  ] /. {{{v_}} :> v, {} :> $Failed}

replaceCachedValue[x_, y_, value_] :=
  SQLExecute[$cache,
    "UPDATE cached_values SET f = ? WHERE x = ? AND y = ?", {value, x, y}
  ]

clearCache[] :=
  SQLExecute[$cache,
    "DELETE FROM cached_values"
  ]

showCache[minX_, maxX_, minY_, maxY_] :=
  SQLExecute[$cache,
    "SELECT *
     FROM cached_values
     WHERE x BETWEEN ? AND ?
     AND y BETWEEN ? AND ?
     ORDER BY x, y"
  , {minX, maxX, minY, maxY}
  , "ShowColumnHeadings" -> True
  ] // TableForm

Этот код SQL использует значения с плавающей запятой в качестве первичных ключей. Обычно это обычная практика в SQL, но в настоящем контексте она отлично работает.

Вы должны позвонить openCacheConnection[], прежде чем пытаться использовать любую из этих функций. Вы должны позвонить closeCacheConnection[] после того, как закончите. Только один раз вы должны вызвать createCache[] для инициализации базы данных SQL. replaceCachedValue, clearCache и showCache предоставляются для ручных вмешательств.

Ответ 2

Самый простой и, возможно, самый эффективный способ сделать это - просто настроить кешированные значения как специальные определения случаев для вашей функции. Поиск довольно быстро из-за хеширования.

Функция:

In[1]:= f[x_, y_] := Cos[x] + Cos[y]

Какие точки используются во время ContourPlot?

In[2]:= points = Last[
   Last[Reap[
     ContourPlot[f[x, y], {x, 0, 4 Pi}, {y, 0, 4 Pi}, 
      EvaluationMonitor :> Sow[{x, y}]]]]];

In[3]:= Length[points]

Out[3]= 10417

Настройте версию f с предварительно вычисленными значениями для 10000 оценок:

In[4]:= Do[With[{x = First[p], y = Last[p]}, precomputedf[x, y] = f[x, y];], {p, 
   Take[points, 10000]}];

В приведенном выше примере вы должны использовать что-то вроде precomputedf[x, y] = z вместо precomputed[x, y] = f[x, y], где z - ваше предварительно вычисленное значение, которое вы сохранили во внешнем файле.

Вот случай "else", который просто оценивает f:

In[5]:= precomputedf[x_, y_] := f[x, y]

Сравнить тайминги:

In[6]:= ContourPlot[f[x, y], {x, 0, 4 Pi}, {y, 0, 4 Pi}]; // Timing

Out[6]= {0.453539, Null}

In[7]:= ContourPlot[precomputedf[x, y], {x, 0, 4 Pi}, {y, 0, 4 Pi}]; // Timing

Out[7]= {0.440996, Null}

Не большая разница во времени, потому что в этом примере f не является дорогостоящей функцией.

Отдельное замечание для вашего конкретного приложения: Возможно, вы могли бы использовать ListContourPlot. Затем вы можете точно определить, какие точки оцениваются.