Пакет Юлии DrWatson.jl направлен на научные вычисления и анализ данных. Он описывает себя как «программное обеспечение для помощников по научным проектам». Недавно я решил попробовать, и мне пока нравится функциональность.
К полезным функциям, на мой взгляд, относятся: safesave
метод, который сохраняет словари в JLD
файлы, убедившись, что данные не потеряны, а @tag!
макрос, который добавляет пары ключ-значение в словарь, которые включают, например, текущий хэш фиксации git, а также предупреждение вместе с «gitpatch», показывающим различия, в случае грязного репозитория. Он также предоставляет инструменты для сбора результатов всех этих файлов вместе в DataFrame
объекты.
Я использую сценарии с множеством настраиваемых параметров, которые необходимо запускать на кластерных машинах в течение нескольких дней для вывода результатов. Вскоре после запуска данной симуляции я хочу быть как можно более уверенным в том, что (при условии, что симуляция работает нормально) моя программа не выйдет из строя в конце при попытке сохранить данные, и что легко отслеживать, где находятся файлы результатов пошел, глядя на консольный вывод запуска.
Мой текущий рабочий процесс склоняется к использованию DrWatson
просто небрежно сбросить все входные параметры и все результаты моих симуляций в .jld
файлы как Dict
объекты. Может быть лучше использовать какой-нибудь формат на основе JSON для сохранения всех параметров моделирования. Однако у меня есть много разных симуляций, которые в основном имеют непересекающиеся наборы входных параметров и результатов. В целом, время, потраченное на разработку какой-то системы для сохранения всех метаданных, кажется серьезным отвлечением от моей реальной работы (это то, что она сказала, когда писала сообщение CodeReview об инструментах для упрощения и автоматизации рабочего процесса …).
Проблема
Пока DrWatson
кажется полезным, но, приняв его в свою кодовую базу, я закончил тем, что создал безумное количество шаблонного кода. Репозиторий, содержащий сценарии моделирования, не содержит кода моделирования и ссылается на другие репозитории (пакеты Julia). Таким образом, мой проект «моделирования» состоит из множества полностью независимых и непересекающихся скриптов, каждый из которых выглядел следующим образом (обратите внимание на шаблон):
using DrWatson
@quickactivate "PROJECTNAME"
println(repeat("-", 80))
println(repeat("-", 10), " Script $(@__FILE__)")
println(repeat("-", 80))
using SparseArrays
using JLD
using Pkg
@info("Status of custom simulation packages used:")
Pkg.status("<SOMESTUFF>")
# ...
function main()
p = 32 # blahblahblah
q = 4 # ...
# many more parameters...
# some of these parameters get organized into `struct`'s
simul_params = DrWatson.@strdict p q # a LOT more stuff.
# Multiple lines of listed parameters, easy to forget something...
@tag!(simul_params) # add git commit information, etc.
output_file_name = DrWatson.savename(<important parameters>, ".jld")
println("Output file complete results: '$output_file_name'")
# ... simulations can take days to complete
# some of the simulations actually internally save intermediate results
# to paths that are part of the parameters.
result1 = external_library.some_simulation(parameters)
result2 = external_library.other_simulation(result1, other_parameters)
simul_params["result1"] = result1
simul_params["result2"] = result2
# ...
# you get the idea
output_file_path = DrWatson.datadir("sims", "script_name", filename), simul_params)
DrWatson.safesave(output_file_path, simul_params)
end
main()
Предложенное решение
То, что я в итоге сделал, немного похоже на этот пост с обзором кода (на самом деле я не совсем понимаю, что там делается). Однако, используя DrWatson
Я получаю гораздо больше метаданных, и мой подход кажется гораздо более радикальным, поскольку я экономлю все доступные переменные. Возможно, такой подход, как связанный пост, на самом деле объективно лучше, я не уверен, насколько опасны / недостатки экономии абсолютно все вернется, чтобы укусить меня.
В любом случае приведенный ниже код определяет макросы, которые либо возвращаются как Dict
(или сохраните прямо в JLD
файл) все глобальные и локальные переменные (кроме функций, модулей, переменных со специальными именами и т. д.). Это делается с помощью Base.@local
чтобы получить локальные переменные в области действия вызывающего и getfield
для получения глобальных переменных вызывающего модуля. Использование getfield
вдохновлен источником JLD2.jl.
"""
This file is to be `include`'d into all simulation scripts.
It makes sure that the DrWatson package and its required imports are available.
"""
using DrWatson
@quickactivate "LDPC4QKD"
using SparseArrays
using JLD
using Pkg
@info("Status of RateAdaptiveLDPC used:")
Pkg.status("RateAdaptiveLDPC")
"""
Provides macros for obtaining `Dict`'s of selected local (potentially also global) variables.
"""
module ScopeCrawler
using DrWatson
using SparseArrays
using JLD
export @all_vars_dict
export @safesave_vars
"""
macro safesave_vars(out_path="", save_globals=true)
Saves the outputs of `@all_vars_dict` to a `JLD` file as a `Dict{String, Any}`.
"""
macro safesave_vars(out_path="", save_globals=true)
return quote
output_file_path = $(esc(out_path))
# auto-generate path if none given.
if output_file_path == ""
output_file_path = datadir("autoname", "$(basename(string(__source__.file)))_line$(__source__.line)_$(__module__).jld")
@warn("@safesave_locals received empty destination path from $(__source__.file). Auto-generated path:
'$output_file_path'")
end
# make sure the file path has '.jld' extension.
if length(output_file_path) < 4 || output_file_path[end - 3:end] != ".jld"
@warn("Appending extension '.jld' to output file path '$output_file_path'.")
output_file_path *= ".jld"
end
results = @all_vars_dict($(esc(save_globals)))
DrWatson.@tag!(results)
if isfile(output_file_path)
@warn("The requested output path $output_file_path already exists.
Final path is chosen by `DrWatson.safesave` and will (probably) be
$(abspath(DrWatson.recursively_clear_path(output_file_path)))")
else
@info("Final output file destination: $(abspath(output_file_path))")
end
DrWatson.safesave(
output_file_path,
results
)
end
end
"""
macro all_vars_dict(save_globals=true)
Returns all local variables (with reasonable names, no functions and no modules)
in the caller scope and global variables of the module in a `Dict{String, Any}`
(with some additional meta-information).
Note: this is slow and not optimized. Meant to be called rarely!
TODO: test properly
TODO specify additional information
"""
macro all_vars_dict(save_globals=true)
# Note: output_file_path should evaluate to a String.
sourcefile = "unknown"
sourceline = "unknown"
try
sourcefile = basename(string(__source__.file))
sourceline = __source__.line
catch e
@warn("macro received invalid __source__: $e")
end
# could add additional metadata that the macro has access to
additional_info = Dict{String,Any}()
try
additional_info["#source#"] = repr(__source__)
additional_info["#module#"] = repr(__module__)
additional_info["#stacktrace#"] = stacktrace()
catch e
@warn("Failed to privide additional info in safesave_locals. Reason: $e")
end
# helper functions
symbol_dict_to_strdict(dict::Dict{Symbol,T} where T) = Dict(string(key) => val for (key, val) in dict)
function combine_conflicting_keys(x, y) # when globals and conflict, resolve
@warn("Name conflict found between global/local variables while saving: $x vs $y.")
return Dict("local" => x, "global" => y)
end
# Generate expression.
# Basic idea:
# Evaluate `Base.@locals` and function `get_globals_dict` in the scope of the caller.
return quote
globals_dict = Dict{String, Any}()
if $(esc(save_globals))
globals_dict = $get_globals_dict(@__MODULE__)
end
merge($combine_conflicting_keys,
# `Any` makes sure the right method is for `merge` is called:
Dict{String, Any}(),
$symbol_dict_to_strdict(Base.@locals),
$additional_info,
globals_dict
)
end
end
"""
function get_globals_dict(m::Module)
Returns a `Dict{String, Any}` containing global fields of a module that are likely to be user-defined variables.
Ignores modules, functions and non-standard names.
Note: inspired from JLD2.jl,
see https://github.com/JuliaIO/JLD2.jl/blob/f7535ab19cb65e703ce4f1f2209ccd32e5b9287f/src/loadsave.jl#L78
"""
function get_globals_dict(m::Module)
results = Dict{String,Any}()
try
for vname in names(m; all=true)
s = string(vname)
if (!occursin(r"^_+[0-9]*$", s) # skip IJulia history vars
&& !occursin(r"[@#$%^&*()?|\/,.<>]", s)) # skip variables with special characters
v = getfield(m, vname)
if !isa(v, Module) && !isa(v, Function)
try
results[s] = v
catch e
if isa(e, PointerException)
@warn("Saving globals, skipping $vname because it contains a pointer.")
else
@warn("Saving globals, skipping $vname because $e")
end
end
end
end
end
catch e
@warn("Saving globals failed because: $e")
end
return results
end
end # module ScopeCrawler
"""
poor man's unit tests for the above
Note: running this test at the start of each job using the functionality makes sense
because if this stuff crashes at the end of a simulation, all the work is lost.
"""
module TestingScopeCrawler
using Test
using ..ScopeCrawler
using JLD
tmptmp_global_var_for_test = "tmptmp_global_var_for_test"
@testset "testing @all_vars_dict" begin
local_var = 3
othervar = "asdf"
@testset "no args" begin
allvarsdict = @all_vars_dict
@test allvarsdict["local_var"] == 3
@test allvarsdict["othervar"] == "asdf"
@test allvarsdict["tmptmp_global_var_for_test"] == "tmptmp_global_var_for_test"
end
@testset "arg: true" begin
allvarsdict = @all_vars_dict true
@test allvarsdict["local_var"] == 3
@test allvarsdict["othervar"] == "asdf"
@test allvarsdict["tmptmp_global_var_for_test"] == "tmptmp_global_var_for_test"
end
@testset "arg: false" begin
allvarsdict = @all_vars_dict false
@test allvarsdict["local_var"] == 3
@test allvarsdict["othervar"] == "asdf"
@test_throws(KeyError, allvarsdict["tmptmp_global_var_for_test"] == "tmptmp_global_var_for_test")
end
@testset "arg: variable name" begin
variable_containing_false = false
allvarsdict = @all_vars_dict variable_containing_false
@test allvarsdict["local_var"] == 3
@test allvarsdict["othervar"] == "asdf"
@test_throws(KeyError, allvarsdict["tmptmp_global_var_for_test"] == "tmptmp_global_var_for_test")
end
end
@testset "testing @safesave_locals" begin
try
local_var = 3
othervar = "asdf"
dest_path = "tmptmp_test_safesave_locals.jld"
@safesave_vars dest_path
@test isfile("tmptmp_test_safesave_locals.jld")
loadagain = load("tmptmp_test_safesave_locals.jld")
@test loadagain["local_var"] == 3
@test loadagain["othervar"] == "asdf"
@test loadagain["tmptmp_global_var_for_test"] == "tmptmp_global_var_for_test"
finally
rm("tmptmp_test_safesave_locals.jld") # throws exception `IOError` if the file does not exist.
end
end
end # module TestingScopeCrawler
Как использовать:
С моим стандартным скриптом приведенный выше код упрощается до
include("dr_watson_boilerplate.jl")
function main()
p = 32 # blahblahblah
q = 4 # ...
# many more parameters...
# some of these parameters get organized into `struct`'s
output_file_name = DrWatson.savename(<important parameters>, ".jld")
println("Output file complete results: '$output_file_name'")
# ... simulations can take days to complete
# some of the simulations actually internally save intermediate results
# to paths that are part of the parameters.
result1 = external_library.some_simulation(parameters)
result2 = external_library.other_simulation(result1, other_parameters)
output_file_path = DrWatson.datadir("sims", "script_name", filename), simul_params)
ScopeCrawler.@safesave_vars output_file_path
end
main()
Что вы думаете?