Add perturbation statistics experiment generation and processing scripts

Sun, 04 Oct 2026 16:30:24 -0500

author
Tuomo Valkonen <tuomov@iki.fi>
date
Sun, 04 Oct 2026 16:30:24 -0500
changeset 13
2cb7f20654e6
parent 12
a0d61ac3719e
child 14
0fffcda184a2
child 15
7c63af7c9acd

Add perturbation statistics experiment generation and processing scripts

.hgignore file | annotate | diff | comparison | revisions
stats/base.py file | annotate | diff | comparison | revisions
stats/extract_results.sh file | annotate | diff | comparison | revisions
stats/generate.sh file | annotate | diff | comparison | revisions
stats/run.sh file | annotate | diff | comparison | revisions
stats/summarise.jl file | annotate | diff | comparison | revisions
--- a/.hgignore	Sun Oct 04 13:02:26 2026 -0500
+++ b/.hgignore	Sun Oct 04 16:30:24 2026 -0500
@@ -13,6 +13,10 @@
 .venv/
 basix-*/
 dolfinx-*/
-res
-res_*/
 results/
+results_*/
+tmp/
+__old*
+experiments/variant*
+REMAINING
+REMAINING.lock
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stats/base.py	Sun Oct 04 16:30:24 2026 -0500
@@ -0,0 +1,60 @@
+import laser_and_mirrors_aux
+
+# Give name to the problem
+name = "laser_and_mirrors_aux"
+
+# Fine-tune algorithms
+alg_finetune = {
+    "tau0": 0.02,
+    "sigmap0": 0.0001,
+    "tautheta": 0.07,
+    "gradv_lipest_mult": 1,
+    "transport_tolerance": 1e5,
+    "inner_method": "Exact",
+    "merge": True,
+    "fitness_merging": True,
+    "merge_radius": 0.1,
+    "merge_interp": False,
+    "scaling_heuristic": None,
+    "extra_weight_optimisation_steps": None,
+}
+
+reg_params = {
+    "alpha": 2e-3,
+    "alpha_k": 60,
+    "alpha_c": 0.06,
+    "c_noise": 0.02,
+    "k_noise": 0.02,
+    "seed": 2393,
+    "true_init": False,
+    "true_target": False,
+    "noise": 0.01,
+    "sim_oversample": 2,
+    "sim_oversample_time": 2,
+    "scale_cstep": 300,
+    "do_non_sliding": True,
+}
+
+algorithm_overrides = {
+    "RadonFB": alg_finetune,
+    "RadonSlidingFB": alg_finetune,
+    "RadonPDPS": alg_finetune,
+    "RadonSlidingPDPS": alg_finetune,
+}
+
+
+def setup(prefix):
+    return laser_and_mirrors_aux.setup(
+        prefix,
+        alpha=reg_params["alpha"],
+        alpha_k=reg_params["alpha_k"],
+        alpha_c=reg_params["alpha_c"],
+        c_noise=reg_params["c_noise"],
+        k_noise=reg_params["k_noise"],
+        seed=reg_params["seed"],
+        true_init=reg_params["true_init"],
+        true_target=reg_params["true_target"],
+        sim_oversample=reg_params["sim_oversample"],
+        sim_oversample_time=reg_params["sim_oversample_time"],
+        scale_cstep=reg_params["scale_cstep"],
+    )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stats/extract_results.sh	Sun Oct 04 16:30:24 2026 -0500
@@ -0,0 +1,86 @@
+#!/bin/sh
+iter=${MAXITER:-5000}
+files=(radon_sliding_fb_log.txt radon_fb_log.txt radon_sliding_fb_config.json radon_fb_config.json radon_sliding_fb_stats.json radon_fb_stats.json)
+tmp=tmp
+
+this_script=$(realpath "$0")
+dir=$(dirname "$this_script")
+top="$dir/../"
+
+trap 'kill -- -$$' INT
+
+function generate {
+    i="$2"
+    job=$(basename "$i")
+    dest=$top/$tmp/"$i"
+    if echo "$i" | grep do_non_sliding=False  > /dev/null; then
+        do_non_sliding=false
+    else
+        do_non_sliding=true
+    fi
+
+    if ! test "$1" = partial; then
+        mkdir -p "$dest"
+        src1=$i/laser_and_mirrors_aux
+        src2=$i/laser_and_mirrors_aux2
+
+        $top/plot.py ${src1}/radon_sliding_fb --simple --iter ${iter} --save-only
+        $top/extract_aux.py ${src1}/radon_sliding_fb
+        cp ${src1}/radon_sliding_fb/res_${iter}.pdf ${dest}/aux_res_sliding_${iter}.pdf
+        cp ${src1}/radon_sliding_fb/true_${iter}.pdf ${dest}/aux_true_${iter}.pdf
+        cp ${src1}/radon_sliding_fb_log.txt ${dest}/aux_radon_sliding_fb_log.txt
+        cp ${src1}/radon_sliding_fb/res_aux.csv ${dest}/aux_radon_sliding_fb_aux.csv
+
+        if "$do_non_sliding"; then
+            $top/plot.py ${src1}/radon_fb --simple --iter ${iter} --save-only
+            $top/extract_aux.py ${src1}/radon_fb
+            cp ${src1}/radon_fb/res_${iter}.pdf ${dest}/aux_res_${iter}.pdf
+            cp ${src1}/radon_fb_log.txt ${dest}/aux_radon_fb_log.txt
+            cp ${src1}/radon_fb/res_aux.csv ${dest}/aux_radon_fb_aux.csv
+        fi
+
+        cp ${src1}/init_aux.csv ${dest}/aux_init_aux.csv
+        for file in $files; do
+            cp ${src1}/$file ${dest}/aux_$file
+        done
+
+        $top/plot.py ${src2}/radon_sliding_fb --simple --iter ${iter} --save-only
+        $top/extract_aux.py ${src2}/radon_sliding_fb
+        cp ${src2}/radon_sliding_fb/res_${iter}.pdf ${dest}/aux2_res_sliding_${iter}.pdf
+        cp ${src2}/radon_sliding_fb/true_${iter}.pdf ${dest}/aux2_true_${iter}.pdf
+        cp ${src2}/radon_sliding_fb_log.txt ${dest}/aux2_radon_sliding_fb_log.txt
+        cp ${src2}/radon_sliding_fb/res_aux.csv ${dest}/aux2_radon_sliding_fb_aux.csv
+
+        if "$do_non_sliding"; then
+            $top/plot.py ${src2}/radon_fb --simple --iter ${iter} --save-only
+            $top/extract_aux.py ${src2}/radon_fb
+            cp ${src2}/radon_fb/res_${iter}.pdf ${dest}/aux2_res_${iter}.pdf
+            cp ${src2}/radon_fb_log.txt ${dest}/aux2_radon_fb_log.txt
+            cp ${src2}/radon_fb/res_aux.csv ${dest}/aux2_radon_fb_aux.csv
+        fi
+
+        cp ${src2}/init_aux.csv ${dest}/aux2_init_aux.csv
+        for file in $files; do
+            cp ${src2}/$file ${dest}/aux2_$file
+        done
+    fi
+    (cd $dest; pdflatex -jobname="$job" '\def\maxiter{'$iter'}\input{../plotnew}')
+}
+
+for i in "$@"; do
+    echo "Generating ${i}…"
+    generate "$GEN_MANY_PARTIAL" "$i" &
+done
+
+wait
+
+files=()
+for i in "$@"; do
+    fn="$tmp/$i/$i.pdf"
+    if test -f "$fn"; then
+        files+=("$fn")
+    fi
+done
+#open "${files[@]}"
+echo "${files[@]}"
+pdfjam -o fused.pdf "${files[@]}"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stats/generate.sh	Sun Oct 04 16:30:24 2026 -0500
@@ -0,0 +1,45 @@
+#!/bin/sh
+
+function rep {
+    b="$1.py"
+    shift
+    p="$1"
+    shift
+    expr=""
+    name="$p"
+    comma=""
+    var="$1"
+    shift
+    while test "$var" != "--"; do
+        val="$1"
+        shift
+        expr="$expr s/\"$var\" *:.*/\"$var\": $val,/;"
+        name="$name$comma$var=$val"
+        comma=","
+        var="$1"
+        shift
+    done
+    var="$1"
+    shift
+    for val in "$@"; do
+        sed "$expr; s/\"$var\" *:.*/\"$var\": $val,/" $b > "$name$comma$var=$val].py"
+    done
+}
+
+this_script=$(realpath "$0")
+dir=$(dirname "$this_script")
+
+prefix="$dir/../experiments/variant["
+base="$dir/base"
+
+for seed in $(seq 1 8); do
+    # Most parameters halved or doubled.
+    # Step length parameters halved or divided by four, due to being chosen as high as works.
+    rep $base $prefix -- seed $seed
+    rep $base $prefix seed $seed -- tau0 0.01 0.005
+    rep $base $prefix seed $seed -- sigmap0 0.00005 0.0002
+    rep $base $prefix seed $seed -- alpha 1e-3 4e-3
+    rep $base $prefix seed $seed -- alpha_k 30 120
+    rep $base $prefix seed $seed -- alpha_c 0.03 0.12
+    rep $base $prefix seed $seed do_non_sliding False -- tautheta 0.0175 0.035
+done
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stats/run.sh	Sun Oct 04 16:30:24 2026 -0500
@@ -0,0 +1,35 @@
+#!/bin/sh
+
+iters=5000
+
+set -e
+
+for f in "$@"; do
+    if ! test -f "$f"; then
+        echo "$f does not exist"
+        exit 1
+    fi
+done
+
+i=1
+n=$#
+for f in "$@"; do
+    a=$(basename "$f" .py)
+    #prefix="results_$(hg id -i)_$(hg --repo=../pointsource_algs id -i)_$(hg --repo=../measures id -i)"
+    prefix="results"
+    printf '\033]2;%s\033\\' "${prefix}_$a"
+    tmp=$(mktemp)
+    sed "s/laser_and_mirrors/laser_and_mirrors2/; s/mirrors2_aux/mirrors_aux2/;" "$f" > "$tmp"
+    if echo "$a" | grep do_non_sliding=False  > /dev/null; then
+        nice cargo run --release  --   -o "${prefix}_$a" -a radon_sliding_fb --max-iter $iters "$f" "$tmp"
+    else
+        nice cargo run --release  --   -o "${prefix}_$a" -a radon_sliding_fb -a radon_fb --max-iter $iters "$f" "$tmp"
+    fi
+    #echo "Results in ${prefix}_$a"
+    i=$(expr $i + 1)
+done
+
+for f in "$@"; do
+    a=$(basename "$f" .py)
+    echo "Results in ${prefix}_$a"
+done
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/stats/summarise.jl	Sun Oct 04 16:30:24 2026 -0500
@@ -0,0 +1,186 @@
+#!/usr/bin/env julia
+
+using CSV
+using DataFrames
+using Statistics
+using Distributions
+
+# --------------------------------------------------------------------------
+# CONFIGURATION
+#
+# Map from CSV file name (as it appears inside each parameter directory) to
+# the list of data columns in that file that should be summarized. Files may
+# have different columns from one another; the `iter` column is assumed to
+# be present in all of them and is NOT listed here.
+# --------------------------------------------------------------------------
+const LOG_COLS = ["value", "relative_value", "discrepancy", "aux_discrepancy", "joint_discrepancy"];
+const AUX_COLS = ["k", "c1", "c2", "k_dist", "c_dist"]
+
+const FILES_COLUMNS = Dict(
+    "aux_radon_fb_log.txt" => LOG_COLS,
+    "aux_radon_sliding_fb_log.txt" => LOG_COLS,
+    "aux2_radon_fb_log.txt" => LOG_COLS,
+    "aux2_radon_sliding_fb_log.txt" => LOG_COLS,
+    "aux_radon_fb_aux.csv" => AUX_COLS,
+    "aux_radon_sliding_fb_aux.csv" => AUX_COLS,
+    "aux2_radon_fb_aux.csv" => AUX_COLS,
+    "aux2_radon_sliding_fb_aux.csv" => AUX_COLS,
+)
+
+const ITER_COL = :iter
+const CONF_LEVEL = 0.95
+
+# --------------------------------------------------------------------------
+# Compute the half-width of a 95% (or CONF_LEVEL) confidence interval for
+# the mean of `values`, using the t-distribution. Falls back gracefully for
+# degenerate sample sizes.
+# --------------------------------------------------------------------------
+function conf_margin(values::AbstractVector{<:Real})
+    n = length(values)
+    if n < 2
+        return 0.0
+    end
+    s = std(values)
+    if s == 0
+        return 0.0
+    end
+    tdist = TDist(n - 1)
+    tcrit = quantile(tdist, 1 - (1 - CONF_LEVEL) / 2)
+    return tcrit * s / sqrt(n)
+end
+
+# --------------------------------------------------------------------------
+# Read one configured file from one parameter directory, if it exists.
+# Returns a DataFrame with at least `iter` and the configured columns, or
+# `nothing` if the file is missing or unreadable.
+# --------------------------------------------------------------------------
+function read_dir_file(dir::AbstractString, filename::AbstractString, cols::Vector{String})
+    path = joinpath(dir, filename)
+    if !isfile(path)
+        @warn "Missing file, skipping" path
+        return nothing
+    end
+
+    local df
+    try
+        df = CSV.read(path, DataFrame)
+    catch e
+        @warn "Could not read CSV, skipping" path exception = e
+        return nothing
+    end
+
+    if !hasproperty(df, ITER_COL)
+        @warn "File has no '$(ITER_COL)' column, skipping" path
+        return nothing
+    end
+
+    present_cols = String[]
+    for c in cols
+        if hasproperty(df, Symbol(c))
+            push!(present_cols, c)
+        else
+            @warn "Column '$c' not found in file, skipping that column for this directory" path
+        end
+    end
+    if isempty(present_cols)
+        return nothing
+    end
+
+    keep = vcat([ITER_COL], Symbol.(present_cols))
+    return df[:, keep]
+end
+
+# --------------------------------------------------------------------------
+# Build the summary DataFrame for one configured file, across all `dirs`.
+# --------------------------------------------------------------------------
+function summarize_file(dirs::Vector{String}, filename::AbstractString, cols::Vector{String})
+    parts = DataFrame[]
+    for dir in dirs
+        d = read_dir_file(dir, filename, cols)
+        d === nothing || push!(parts, d)
+    end
+
+    if isempty(parts)
+        @warn "No data found for file across any directory, skipping summary" filename
+        return nothing
+    end
+
+    combined = vcat(parts...; cols=:union)
+    grouped = groupby(combined, ITER_COL)
+
+    iters = sort(collect(keys(grouped)))
+    summary = DataFrame(iter=[k[ITER_COL] for k in iters])
+
+    for col in cols
+        colsym = Symbol(col)
+        mins = Vector{Union{Missing,Float64}}(undef, length(iters))
+        maxs = Vector{Union{Missing,Float64}}(undef, length(iters))
+        means = Vector{Union{Missing,Float64}}(undef, length(iters))
+        confmin = Vector{Union{Missing,Float64}}(undef, length(iters))
+        confmax = Vector{Union{Missing,Float64}}(undef, length(iters))
+
+        for (i, key) in enumerate(iters)
+            sub = grouped[key]
+            if !hasproperty(sub, colsym)
+                mins[i] = maxs[i] = means[i] = confmin[i] = confmax[i] = missing
+                continue
+            end
+            vals = collect(skipmissing(sub[!, colsym]))
+            if isempty(vals)
+                mins[i] = maxs[i] = means[i] = confmin[i] = confmax[i] = missing
+                continue
+            end
+            m = mean(vals)
+            margin = conf_margin(vals)
+            mins[i] = minimum(vals)
+            maxs[i] = maximum(vals)
+            means[i] = m
+            confmin[i] = m - margin
+            confmax[i] = m + margin
+        end
+
+        summary[!, Symbol(col, "_min")] = mins
+        summary[!, Symbol(col, "_max")] = maxs
+        summary[!, Symbol(col, "_confmax")] = confmax
+        summary[!, Symbol(col, "_confmin")] = confmin
+        summary[!, Symbol(col, "_mean")] = means
+    end
+
+    return summary
+end
+
+# --------------------------------------------------------------------------
+# Main
+# --------------------------------------------------------------------------
+function main()
+    if isempty(ARGS)
+        println(stderr, "Usage: julia summarize.jl DIR1 [DIR2 DIR3 ...]")
+        exit(1)
+    end
+
+    dirs = String[]
+    for d in ARGS
+        if isdir(d)
+            push!(dirs, d)
+        else
+            @warn "Not a directory, skipping" d
+        end
+    end
+
+    if isempty(dirs)
+        println(stderr, "No valid directories given.")
+        exit(1)
+    end
+
+    for (filename, cols) in FILES_COLUMNS
+        summary = summarize_file(dirs, filename, cols)
+        summary === nothing && continue
+
+        stem = first(splitext(filename))
+        outpath = joinpath(pwd(), "$(stem)_summary.csv")
+        CSV.write(outpath, summary)
+        println("Wrote $outpath ($(nrow(summary)) rows)")
+    end
+end
+
+main()

mercurial