src/provenance.jl

Snapshot from this documentation build. SHA-256: 7bb02325e4e65099495bf5dc4bcef59e0a6e11ff1a42d1a984bc956183e3a924

    1 "SHA256 of all implementation sources and dependency declarations, independent of checkout path."
    2 function source_fingerprint(root::AbstractString=PACKAGE_ROOT)
    3     files = String[]
    4     for folder in ("src", "lib", "scripts")
    5         for (directory, _, names) in walkdir(joinpath(root, folder))
    6             for name in names
    7                 (endswith(name,".jl") || name == "Project.toml") &&
    8                     push!(files, relpath(joinpath(directory,name),root))
    9             end
   10         end
   11     end
   12     push!(files,"Project.toml")
   13     io = IOBuffer()
   14     for name in sort(files)
   15         print(io,name,'\0',bytes2hex(sha256(read(joinpath(root,name)))),'\n')
   16     end
   17     bytes2hex(sha256(take!(io)))
   18 end
   19 
   20 function _run_metadata(config, env; status="RUNNING")
   21     project = Base.active_project()
   22     manifest = project === nothing ? "" : joinpath(dirname(project),"Manifest.toml")
   23     solver = lowercase(get(env,"MHD_SOLVER","glm"))
   24     ctnote = solver == "ct" ? Dict{String,Any}(
   25         "ct_step_glm_limited"=>false,"ct_glm_project_every"=>0,
   26         "ct_note"=>"CT dt is the plain CFL dt (not shrunk by ch=glm_ch_fac*smax) and B is never " *
   27                    "projected; GLM radiation runs project every cycle. Expected difference at equal CFL.") :
   28         Dict{String,Any}()
   29     merge(Dict("schema_version"=>1,"status"=>status,"mhd_solver"=>solver,"created_utc"=>string(now(UTC)),
   30         "machine"=>gethostname(),"kernel"=>string(Sys.KERNEL),"architecture"=>string(Sys.ARCH),
   31         "julia_version"=>string(VERSION),"julia_threads"=>Threads.nthreads(),
   32         "source_fingerprint_sha256"=>source_fingerprint(),
   33         "active_project"=>something(project,""),
   34         "manifest_sha256"=>isfile(manifest) ? bytes2hex(sha256(read(manifest))) : "unresolved",
   35         "configuration"=>config.settings,"environment"=>env),ctnote)
   36 end
   37 
   38 function _completion_status(log::AbstractString,env)
   39     haskey(env,"MHD_ZSTART") || return "COMPLETED"
   40     matches = collect(eachmatch(r"COSMO final_z=([+\-0-9.eE]+)",log))
   41     isempty(matches) && return "INCOMPLETE"
   42     z = parse(Float64,last(matches).captures[1])
   43     target = parse(Float64,get(env,"MHD_ZEND","20"))
   44     z <= target + 1e-6*max(1,target) ? "COMPLETED" : "STOPPED_AT_LIMIT"
   45 end
   46 
   47 "CT_INITIAL_FACES numbers from the first run segment of `run.log` (empty if absent)."
   48 function _ct_initial_faces(log::AbstractString)
   49     m = match(r"CT_INITIAL_FACES cell_b_rel_l2_change=([^ ]+) magnetic_energy_ratio=([^ ]+) divb_face_dx_over_brms=([^ ]+)",log)
   50     m === nothing && return Dict{String,Any}()
   51     Dict{String,Any}("ct_initial_cell_b_rel_l2_change"=>parse(Float64,m.captures[1]),
   52         "ct_initial_magnetic_energy_ratio"=>parse(Float64,m.captures[2]),
   53         "ct_initial_divb_face_dx_over_brms"=>parse(Float64,m.captures[3]))
   54 end
   55 
   56 const RESTART_CONTROL_OPTIONS = Set([
   57     "MHD_ZEND", "MHD_STEPS", "MHD_MAX_CYCLES", "MHD_CHECKPOINT_RESUME",
   58     "MHD_CHECKPOINT_EVERY", "MHD_CHECKPOINT_FINAL", "MHD_PRINT_EVERY",
   59     "MHD_CYCLE_PRINT_EVERY", "MHD_CHECK_EVERY", "MHD_PK_ZS", "MHD_PK_EVERY",
   60 ])
   61 
   62 function _validate_restart_environment(previous,current)
   63     changed = sort!([String(key) for key in union(keys(previous),keys(current))
   64         if !(key in RESTART_CONTROL_OPTIONS) &&
   65            get(previous,key,nothing) != get(current,key,nothing)])
   66     isempty(changed) || error("restart changes evolution options: " * join(changed,", ") *
   67         "; use the original physical settings or start a new run")
   68     nothing
   69 end
   70 
   71 """
   72     run_simulation(config; output_dir, dry_run=false)
   73 
   74 Run the full production model in a fresh Julia process using the current active
   75 environment. A fresh process isolates the legacy driver's compile-time options
   76 and device caches. `metadata.toml` and `resolved-environment.toml` record inputs;
   77 `run.log` is flushed by the production driver. Existing runs require explicit
   78 `run.resume=true` and a checkpoint. Run directories are never silently replaced.
   79 """
   80 function run_simulation(c::RunConfig; output_dir::AbstractString, dry_run::Bool=false)
   81     env = run_environment(c; output_dir)
   82     dry_run && return env
   83     out = abspath(output_dir)
   84     existing = isfile(joinpath(out,"metadata.toml"))
   85     resuming = get(env,"MHD_CHECKPOINT_RESUME","false") in ("true","1")
   86     existing && !resuming && error("output already contains a run; choose a new directory or resume=true")
   87     resuming && !isfile(env["MHD_CHECKPOINT_PATH"]) && error("resume requires checkpoint.jls")
   88     mkpath(out)
   89     if existing
   90         previous = TOML.parsefile(joinpath(out,"metadata.toml"))
   91         previous["source_fingerprint_sha256"] == source_fingerprint() ||
   92             error("checkpoint source fingerprint differs; migration requires explicit review")
   93         _validate_restart_environment(previous["environment"],env)
   94     end
   95     metadata = _run_metadata(c,env)
   96     function write_toml(path,payload)
   97         open(io->TOML.print(io,payload;sorted=true),path*".tmp","w")
   98         mv(path*".tmp",path;force=true)
   99     end
  100     write_toml(joinpath(out,"metadata.toml"),metadata)
  101     write_toml(joinpath(out,"resolved-environment.toml"),env)
  102     active = Base.active_project()
  103     if active !== nothing
  104         cp(active,joinpath(out,"Project.toml");force=true)
  105         manifest = joinpath(dirname(active),"Manifest.toml")
  106         isfile(manifest) && cp(manifest,joinpath(out,"Manifest.toml");force=true)
  107     end
  108     # Prevent unrelated shell MHD options from silently modifying the science case.
  109     child_env = Dict(k=>v for (k,v) in ENV if !startswith(k,"MHD_"))
  110     merge!(child_env,env)
  111     project = dirname(something(Base.active_project(),joinpath(PACKAGE_ROOT,"Project.toml")))
  112     script = joinpath(PACKAGE_ROOT,"scripts","_production.jl")
  113     cmd = setenv(`$(Base.julia_cmd()) --project=$project --threads=$(Threads.nthreads()) $script`,child_env)
  114     println("PMFGLM run: ",out,"\nLog: ",joinpath(out,"run.log")); flush(stdout)
  115     t0 = time()
  116     try
  117         open(joinpath(out,"run.log"),resuming ? "a" : "w") do io
  118             process = run(pipeline(ignorestatus(cmd);stdout=io,stderr=io))
  119             success(process) || error("production process exited with status $(process.exitcode); see $(joinpath(out,"run.log"))")
  120         end
  121         log = read(joinpath(out,"run.log"),String)
  122         metadata["status"] = _completion_status(log,env)
  123         metadata["mhd_solver"] == "ct" && merge!(metadata,_ct_initial_faces(log))
  124     catch error
  125         metadata["status"] = "FAILED"
  126         metadata["error"] = sprint(showerror,error)
  127         rethrow()
  128     finally
  129         metadata["wall_seconds"] = time()-t0
  130         metadata["finished_utc"] = string(now(UTC))
  131         write_toml(joinpath(out,"metadata.toml"),metadata)
  132     end
  133     (; output_dir=out, status=metadata["status"], wall_seconds=metadata["wall_seconds"])
  134 end