From 7380e783ffcdd65d2f06141c33790c685a627a4c Mon Sep 17 00:00:00 2001 From: Emanuel Gull Date: Thu, 24 Sep 2026 14:59:43 +0200 Subject: [PATCH 1/6] Add legendre_convert regression coverage --- ALPS3_MIGRATION_PLAN.md | 9 +++-- test/regression/CMakeLists.txt | 14 ++++++++ test/regression/MANIFEST.md | 9 ++++- test/regression/PROVENANCE.md | 9 +++++ test/regression/README.md | 6 ++-- test/regression/REFERENCE_CHANGES.md | 10 ++++++ test/regression/cases.py | 18 ++++++++-- test/regression/compare.py | 3 +- test/regression/generate.py | 10 ++++-- .../legendre_convert_transform/input.dat | 33 ++++++++++++++++++ test/regression/make_inputs.py | 7 ++++ test/regression/manifest.py | 1 + .../reference/legendre_convert_transform.h5 | Bin 0 -> 16568 bytes test/regression/tolerances.json | 2 ++ 14 files changed, 120 insertions(+), 11 deletions(-) create mode 100644 test/regression/inputs/legendre_convert_transform/input.dat create mode 100644 test/regression/reference/legendre_convert_transform.h5 diff --git a/ALPS3_MIGRATION_PLAN.md b/ALPS3_MIGRATION_PLAN.md index 2af9773..4758cac 100644 --- a/ALPS3_MIGRATION_PLAN.md +++ b/ALPS3_MIGRATION_PLAN.md @@ -387,8 +387,10 @@ unchanged. A text-input regression covers trailing blank lines. **2.3B: `legendre_convert`** (decided 2026-09-24), in this order: -1. Add regression cases for `legendre_convert` first (it has no tests), with - references from the current Boost build, like the `kk` cases in 2.2. +1. **Done 2026-09-24:** add a deterministic regression case for + `legendre_convert`, with a reference from the current Boost build. It covers + the transform, tail enforcement, back-continuation, and Matsubara + convergence. 2. Replace Boost.Random by ``. The `mt19937` engine gives identical numbers; the normal variates differ (different algorithm), which is harmless because the error estimate is seeded from the clock. @@ -399,7 +401,8 @@ unchanged. A text-input regression covers trailing blank lines. Keep `boost::math::sph_bessel` (also missing in libc++; our own version would need careful checking at large l and argument) and `program_options` (no -standard equivalent). +standard equivalent). Reconfirmed 2026-09-24: these are intentional retained +Boost boundaries for now. **2.3C: command-line behavior** (updates the CLI references on purpose) diff --git a/test/regression/CMakeLists.txt b/test/regression/CMakeLists.txt index 2d4c14e..97e109b 100644 --- a/test/regression/CMakeLists.txt +++ b/test/regression/CMakeLists.txt @@ -53,6 +53,20 @@ if(MAXENT_BUILD_UTILITIES) ${CMAKE_CURRENT_SOURCE_DIR}/reference ${results} --sets kk) set_tests_properties(regression_kk_generate PROPERTIES FIXTURES_SETUP regression_kk LABELS regression-fast) set_tests_properties(regression_kk_compare PROPERTIES FIXTURES_REQUIRED regression_kk LABELS regression-fast) + + set(results ${CMAKE_CURRENT_BINARY_DIR}/results-legendre-convert) + add_test(NAME regression_legendre_convert_generate + COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/generate.py + --maxent $ + --legendre-convert $ + --out ${results} --sets legendre_convert) + add_test(NAME regression_legendre_convert_compare + COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/compare.py + ${CMAKE_CURRENT_SOURCE_DIR}/reference ${results} --sets legendre_convert) + set_tests_properties(regression_legendre_convert_generate PROPERTIES + FIXTURES_SETUP regression_legendre_convert LABELS regression-fast) + set_tests_properties(regression_legendre_convert_compare PROPERTIES + FIXTURES_REQUIRED regression_legendre_convert LABELS regression-fast) endif() if(MAXENT_REGRESSION_FULL) maxent_regression_test(full full regression-full) diff --git a/test/regression/MANIFEST.md b/test/regression/MANIFEST.md index a0c2b5f..e95483b 100644 --- a/test/regression/MANIFEST.md +++ b/test/regression/MANIFEST.md @@ -85,6 +85,12 @@ Generated by `manifest.py` from `reference/`, `cases.py` and `tolerances.json`. | `kk_imag_to_real_self` | kk: Im Sigma(omega) of self_u1 (average spectrum) (`--input_file=input.dat --output_file=output.dat --imag_to_real`) | `test/regression/inputs/kk_imag_to_real_self` | 6 | 1.1 s | | | `kk_real_to_imag` | kk: Re G(omega) of a unit Gaussian spectrum (Dawson function) (`--input_file=input.dat --output_file=output.dat --real_to_imag`) | `test/regression/inputs/kk_real_to_imag` | 6 | 1.0 s | | +## legendre_convert utility + +| Case | Covers | Inputs | Datasets | Runtime | Flags | +|---|---|---|---|---|---| +| `legendre_convert_transform` | legendre_convert: transform, tails, back-continuation, and Matsubara convergence (`--beta=5 --input_gtau_file=input.dat --output_gl_file=Gl.dat --tail1=1 --tail2=0 --lmax=12 --maxit=10 --backcontinue --plot_convergence=12 --maxn=3 --noerr`) | `test/regression/inputs/legendre_convert_transform` | 9 | 0.3 s | | + ## Components | Case | Covers | Inputs | Datasets | Runtime | Flags | @@ -97,6 +103,7 @@ Rules are matched in order against ':'; the first match applies. | Pattern (case:dataset) | atol | rtol | Why | |---|---|---|---| +| `^legendre_convert:` | 1e-14 | 1e-12 | direct utility regression; allows only rounding-level variation | | `:cli/` | 0 | 0 | command-line output and exit code must not change | | `:log/scalars/(n_singular|max_it_warnings)$` | 0 | 0 | counts; identical in all variants | | `:log/scalars/minimal_chi2$` | 1e-12 | 1e-06 | values below ~1e-12 are rounding noise (observed relative differences up to 1 at 1e-17) | @@ -112,7 +119,7 @@ Rules are matched in order against ':'; the first match applies. | `\.chi2\.dat$` | 0 | 1e-07 | observed 1.7e-9 | | `:files/` | 1e-15 | 2e-06 | spectra, back-continuations, grids, all other outputs; observed 1.5e-7 (GCC 15, u2_time_fast) | -Total compared datasets: 1183 in 59 cases. +Total compared datasets: 1192 in 60 cases. ## Known limitations diff --git a/test/regression/PROVENANCE.md b/test/regression/PROVENANCE.md index 7974154..f7de757 100644 --- a/test/regression/PROVENANCE.md +++ b/test/regression/PROVENANCE.md @@ -45,6 +45,15 @@ form of the Legendre kernel reproduces the GSL kernels in `components.h5` to 3.3e-16 relative, all Legendre cases pass, and the new `kk` spline reproduces the `kk` references bit for bit. +## legendre_convert reference (added 2026-09-24) + +`legendre_convert_transform.h5` was generated from `modernize/step2` merge +commit `a57f150` using AppleClang 21, C++17, Boost 1.88, and ALPSCore +`560ae112`. The utility source was unchanged from the Boost implementation. +Its synthetic Gaussian-spectrum input has zero error bars, so the clock-seeded +bootstrap is deterministic; three consecutive generate-and-compare runs +passed. + ## Determinism Two independent generations with the reference build were compared with diff --git a/test/regression/README.md b/test/regression/README.md index a5091b1..eb1dc3e 100644 --- a/test/regression/README.md +++ b/test/regression/README.md @@ -33,6 +33,7 @@ result, and is flagged in [`MANIFEST.md`](MANIFEST.md). | `cli` | `--help`, `--help.models`, `--help.grids`, missing `BETA` | instant | command-line behavior | | `full` | the 11 example runs as shipped | about 1 min (Legendre dominates) | opt-in | | `kk` | the `kk` utility on 3 inputs (Im G and Im Σ from the references, an analytic Re G) | seconds | registered when `MAXENT_BUILD_UTILITIES=ON` | +| `legendre_convert` | deterministic transform, tails, back-continuation and Matsubara convergence | instant | protects the utility before Boost replacements | | components | `components.h5` from `dump_components` | instant | building blocks | ## Running @@ -52,9 +53,10 @@ them against `reference/`. By hand: ```bash python3 test/regression/generate.py --maxent build/maxent \ --components build/test/regression/dump_components --out /tmp/maxent-results \ - [--kk build/kk/kk] [--sets fast,targeted,cli,full,kk] [--cases REGEX] + [--kk build/kk/kk] [--legendre-convert build/legendre_convert/legendre_convert] \ + [--sets fast,targeted,cli,full,kk,legendre_convert] [--cases REGEX] python3 test/regression/compare.py test/regression/reference /tmp/maxent-results \ - [--sets fast,targeted,cli,kk,components] [--cases REGEX] + [--sets fast,targeted,cli,kk,legendre_convert,components] [--cases REGEX] ``` `compare.py` exits with status 1 if any case fails. `--report` prints the diff --git a/test/regression/REFERENCE_CHANGES.md b/test/regression/REFERENCE_CHANGES.md index 0599825..503ff6d 100644 --- a/test/regression/REFERENCE_CHANGES.md +++ b/test/regression/REFERENCE_CHANGES.md @@ -4,6 +4,16 @@ Every regeneration of files in `reference/` is logged here: date, commit, reason, affected cases, and the largest difference to the previous references (from `compare.py --report`). Newest entry first. +## 2026-09-24: legendre_convert reference added + +* **Source:** `modernize/step2` merge commit `a57f150`; the utility is still + the original Boost implementation. +* **Reason:** `legendre_convert` had no tests; a reference was added before + replacing Boost.Random, `factorial`, and `legendre_p` (plan step 2.3B). +* **Cases:** 1 new (`legendre_convert_transform`). No existing reference changed. +* **Coverage:** transform, tail enforcement, back-continuation, and Matsubara + convergence. Zero input errors make the clock-seeded bootstrap deterministic. + ## 2026-09-24: kk references added * **Commit:** `811b3c6` (see [`PROVENANCE.md`](PROVENANCE.md), kk section). diff --git a/test/regression/cases.py b/test/regression/cases.py index 4b39b66..43b89b2 100644 --- a/test/regression/cases.py +++ b/test/regression/cases.py @@ -5,8 +5,8 @@ name unique case name (also the reference file name) set 'full' (shipped examples), 'fast' (examples, reduced size), 'targeted' (synthetic inputs), 'cli' (command-line snapshots), - 'kk' (the kk utility) - program 'maxent' (default) or 'kk' + 'kk' (the kk utility), or 'legendre_convert' + program 'maxent' (default), 'kk', or 'legendre_convert' inputs directory with the input files, relative to the repository root param parameter file inside `inputs` (None for 'cli' cases) args extra command-line arguments (overrides or CLI flags) @@ -86,6 +86,19 @@ ] +LEGENDRE_CONVERT = [ + dict(name="legendre_convert_transform", set="legendre_convert", + program="legendre_convert", + inputs="test/regression/inputs/legendre_convert_transform", + param=None, + args=["--beta=5", "--input_gtau_file=input.dat", "--output_gl_file=Gl.dat", + "--tail1=1", "--tail2=0", "--lmax=12", "--maxit=10", "--backcontinue", + "--plot_convergence=12", "--maxn=3", "--noerr"], + covers="legendre_convert: transform, tails, back-continuation, and Matsubara convergence", + flags=[], expect="ok"), +] + + def all_cases(): cases = [] for name, directory, param, covers in EXAMPLES: @@ -113,6 +126,7 @@ def all_cases(): cases.append(dict(name=name, set="kk", program="kk", inputs="test/regression/inputs/" + name, param=None, args=["--input_file=input.dat", "--output_file=output.dat", direction], covers="kk: " + covers, flags=[], expect="ok")) + cases.extend(LEGENDRE_CONVERT) for c in cases: c.setdefault("program", "maxent") return cases diff --git a/test/regression/compare.py b/test/regression/compare.py index ebb6fe6..aa27e2b 100644 --- a/test/regression/compare.py +++ b/test/regression/compare.py @@ -107,7 +107,8 @@ def main(): ap.add_argument("reference", type=Path) ap.add_argument("result", type=Path) ap.add_argument("--cases", default=".*", help="regular expression on case names") - ap.add_argument("--sets", help="comma-separated sets to compare (full,fast,targeted,cli,kk,components); " + ap.add_argument("--sets", help="comma-separated sets to compare " + "(full,fast,targeted,cli,kk,legendre_convert,components); " "default: all") ap.add_argument("--tolerances", type=Path, default=HERE / "tolerances.json") ap.add_argument("--report", action="store_true", help="print all differences, never fail") diff --git a/test/regression/generate.py b/test/regression/generate.py index 03fb313..b2fdac7 100644 --- a/test/regression/generate.py +++ b/test/regression/generate.py @@ -4,7 +4,8 @@ python3 test/regression/generate.py --maxent build/maxent --out /tmp/results \ [--sets fast,targeted,cli] [--cases REGEX] [--components build/dump_components] \ - [--kk build/kk/kk] [--provenance prov.json] [--jobs 8] + [--kk build/kk/kk] [--legendre-convert build/legendre_convert/legendre_convert] + [--provenance prov.json] [--jobs 8] For every case one HDF5 file /.h5 is written: /files/ every output of the run: datasets of *.out.h5 files are @@ -164,10 +165,13 @@ def main(): ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--maxent", required=True, type=Path) ap.add_argument("--out", required=True, type=Path) - ap.add_argument("--sets", default="fast,targeted,cli", help="comma-separated: full,fast,targeted,cli,kk") + ap.add_argument("--sets", default="fast,targeted,cli", + help="comma-separated: full,fast,targeted,cli,kk,legendre_convert") ap.add_argument("--cases", default=".*", help="regular expression on case names") ap.add_argument("--components", type=Path, help="dump_components binary (adds components.h5)") ap.add_argument("--kk", type=Path, help="kk binary (needed for the 'kk' set)") + ap.add_argument("--legendre-convert", type=Path, + help="legendre_convert binary (needed for the 'legendre_convert' set)") ap.add_argument("--provenance", type=Path, help="JSON file with provenance key/values") ap.add_argument("--jobs", type=int, default=8) ap.add_argument("--timeout", type=float, default=TIMEOUT, help="seconds per case (default %(default)s)") @@ -180,6 +184,8 @@ def main(): programs = {"maxent": args.maxent.resolve()} if args.kk: programs["kk"] = args.kk.resolve() + if args.legendre_convert: + programs["legendre_convert"] = args.legendre_convert.resolve() missing = sorted({c["program"] for c in selected} - set(programs)) if missing: ap.error(f"selected cases need --{missing[0]}") diff --git a/test/regression/inputs/legendre_convert_transform/input.dat b/test/regression/inputs/legendre_convert_transform/input.dat new file mode 100644 index 0000000..c35add9 --- /dev/null +++ b/test/regression/inputs/legendre_convert_transform/input.dat @@ -0,0 +1,33 @@ +0 -0.5 0 +0.15625 -0.44692748857951159 0 +0.3125 -0.40349354067193222 0 +0.46875 -0.36771245306911809 0 +0.625 -0.33807789362503071 0 +0.78125 -0.31343696333630788 0 +0.9375 -0.29290027098410715 0 +1.09375 -0.27577709831967723 0 +1.25 -0.26152823882525922 0 +1.40625 -0.24973142306264473 0 +1.5625 -0.240055809373353 0 +1.71875 -0.23224308243197406 0 +1.875 -0.22609343305447935 0 +2.03125 -0.22145520108143096 0 +2.1875 -0.21821732191815724 0 +2.34375 -0.21630397511678601 0 +2.5 -0.21567102333290911 0 +2.65625 -0.21630397511678601 0 +2.8125 -0.21821732191815724 0 +2.96875 -0.22145520108143105 0 +3.125 -0.22609343305447938 0 +3.28125 -0.23224308243197403 0 +3.4375 -0.24005580937335302 0 +3.59375 -0.24973142306264476 0 +3.75 -0.26152823882525933 0 +3.90625 -0.27577709831967723 0 +4.0625 -0.2929002709841072 0 +4.21875 -0.31343696333630794 0 +4.375 -0.33807789362503071 0 +4.53125 -0.36771245306911809 0 +4.6875 -0.40349354067193222 0 +4.84375 -0.44692748857951159 0 +5 -0.5 0 diff --git a/test/regression/make_inputs.py b/test/regression/make_inputs.py index d4c06bd..cb8033a 100644 --- a/test/regression/make_inputs.py +++ b/test/regression/make_inputs.py @@ -322,6 +322,13 @@ def make(name, case_dir): w = np.linspace(-10, 10, 401) # Re G of a normalized Gaussian spectrum (sigma 1): sqrt(2)/sigma D(w/(sqrt(2) sigma)) write_columns(case_dir / "input.dat", w, np.sqrt(2) * dawsn(w / np.sqrt(2))) + elif name == "legendre_convert_transform": + # Zero error bars make the clock-seeded bootstrap deterministic. The + # data remain nontrivial and exercise all Legendre orders in the test. + w, q, a = spectrum("S1") + tau = np.linspace(0, BETA, 33) + gtau = transform(k_time_fermionic(tau, w), a, q) + write_columns(case_dir / "input.dat", tau, gtau, np.zeros_like(gtau)) elif name == "cli_missing_beta": write_param(case_dir, [("NDAT", 4), ("X_0", 0.1), ("X_1", 0.2), ("X_2", 0.3), ("X_3", 0.4), ("SIGMA_0", 0.5), ("SIGMA_1", 0.5), ("SIGMA_2", 0.5), ("SIGMA_3", 0.5)]) diff --git a/test/regression/manifest.py b/test/regression/manifest.py index 6486505..d5ba99d 100644 --- a/test/regression/manifest.py +++ b/test/regression/manifest.py @@ -23,6 +23,7 @@ ("cli", "Command-line snapshots"), ("full", "Full set (examples as shipped, opt-in)"), ("kk", "kk utility"), + ("legendre_convert", "legendre_convert utility"), ("components", "Components"), ] diff --git a/test/regression/reference/legendre_convert_transform.h5 b/test/regression/reference/legendre_convert_transform.h5 new file mode 100644 index 0000000000000000000000000000000000000000..6882acdaaed0d21485c5cb4329484b27ad350b6f GIT binary patch literal 16568 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 literal 0 HcmV?d00001 diff --git a/test/regression/tolerances.json b/test/regression/tolerances.json index 499e28e..b94ae0d 100644 --- a/test/regression/tolerances.json +++ b/test/regression/tolerances.json @@ -1,6 +1,8 @@ { "description": "Rules are matched in order against ':'; the first match applies. A dataset passes if max|a-b| <= atol + rtol * max|reference|. Values are about 10x the largest difference observed between the reference build and four variant builds of the same source (AppleClang -O0, LAPACK SVD, clang 22, GCC 15), rounded up; see PROVENANCE.md. The Legendre example is extremely ill-conditioned (minimal chi2 ~ 1e-17) and gets looser rules for its alpha probabilities and Ng.", "rules": [ + {"pattern": "^legendre_convert:", "atol": 1e-14, "rtol": 1e-12, + "why": "direct utility regression; allows only rounding-level variation"}, {"pattern": ":cli/", "atol": 0, "rtol": 0, "why": "command-line output and exit code must not change"}, {"pattern": ":log/scalars/(n_singular|max_it_warnings)$", "atol": 0, "rtol": 0, From 8cbd0cf51c49f58b3b3cc329c4ba16bcae2458a4 Mon Sep 17 00:00:00 2001 From: Emanuel Gull Date: Thu, 24 Sep 2026 15:00:40 +0200 Subject: [PATCH 2/6] Replace Boost.Random with standard random --- ALPS3_MIGRATION_PLAN.md | 2 +- legendre_convert/legendre_convert.cpp | 20 +++++--------------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/ALPS3_MIGRATION_PLAN.md b/ALPS3_MIGRATION_PLAN.md index 4758cac..076b597 100644 --- a/ALPS3_MIGRATION_PLAN.md +++ b/ALPS3_MIGRATION_PLAN.md @@ -391,7 +391,7 @@ unchanged. A text-input regression covers trailing blank lines. `legendre_convert`, with a reference from the current Boost build. It covers the transform, tail enforcement, back-continuation, and Matsubara convergence. -2. Replace Boost.Random by ``. The `mt19937` engine gives identical +2. **Done 2026-09-24:** replace Boost.Random by ``. The `mt19937` engine gives identical numbers; the normal variates differ (different algorithm), which is harmless because the error estimate is seeded from the clock. 3. Replace `boost::math::factorial` by a product (only small arguments occur). diff --git a/legendre_convert/legendre_convert.cpp b/legendre_convert/legendre_convert.cpp index cedbf56..f3de2d0 100644 --- a/legendre_convert/legendre_convert.cpp +++ b/legendre_convert/legendre_convert.cpp @@ -11,11 +11,8 @@ #include #include +#include #include -#include -#include -#include -#include #include #include //needed for Legendre transform #include @@ -243,19 +240,12 @@ struct argstruct{ }; ///genereate a normally distrubted noisy vector. //i.e. output[i] = normally dist number with mean=data[i] and stddev=err[i] -vector_type generateGaussNoise(vector_type data, vector_type err,boost::mt19937 &rng){ - - typedef boost::variate_generator > ran_gen; - //notice the & in the first template argument and function rng argument. - //If we omit this, it will compile and run - //however, the numbers will be less(/not) random b/c it will copy the generator - //each time, outputting the mean with some noise, rather than truly random - +vector_type generateGaussNoise(vector_type data, vector_type err,std::mt19937 &rng){ const int N = data.size(); vector_type data_noise(N); for(int i=0;i s(data[i],err[i]); - data_noise[i] = ran_gen(rng,s)(); + std::normal_distribution<> distribution(data[i],err[i]); + data_noise[i] = distribution(rng); } return data_noise; } @@ -268,7 +258,7 @@ return_type bootstrap(double (*f)(vector_type,void*), //and determinging the variation on the output std::vector newData(maxit); std::cout << std::setprecision(14); - boost::mt19937 rng; + std::mt19937 rng; rng.seed(static_cast(std::time(0))); for(int i=0;i Date: Thu, 24 Sep 2026 15:01:47 +0200 Subject: [PATCH 3/6] Replace Boost factorial with local product --- ALPS3_MIGRATION_PLAN.md | 3 ++- legendre_convert/legendre_convert.cpp | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/ALPS3_MIGRATION_PLAN.md b/ALPS3_MIGRATION_PLAN.md index 076b597..383004b 100644 --- a/ALPS3_MIGRATION_PLAN.md +++ b/ALPS3_MIGRATION_PLAN.md @@ -394,7 +394,8 @@ unchanged. A text-input regression covers trailing blank lines. 2. **Done 2026-09-24:** replace Boost.Random by ``. The `mt19937` engine gives identical numbers; the normal variates differ (different algorithm), which is harmless because the error estimate is seeded from the clock. -3. Replace `boost::math::factorial` by a product (only small arguments occur). +3. **Done 2026-09-24:** replace `boost::math::factorial` by a product (only + small arguments occur). 4. Replace `boost::math::legendre_p` by the standard three-term recurrence. `std::legendre` is not an option: libc++ (AppleClang) does not implement the C++17 special math functions. diff --git a/legendre_convert/legendre_convert.cpp b/legendre_convert/legendre_convert.cpp index f3de2d0..20827c8 100644 --- a/legendre_convert/legendre_convert.cpp +++ b/legendre_convert/legendre_convert.cpp @@ -13,7 +13,6 @@ #include #include #include -#include #include //needed for Legendre transform #include @@ -35,7 +34,10 @@ double tl(int l, int p){ double qsum =1; for(int q=-p+2;q(p-1); + double factorial = 1.0; + for(int factor=2;factor Date: Thu, 24 Sep 2026 15:03:05 +0200 Subject: [PATCH 4/6] Replace Boost Legendre polynomial --- ALPS3_MIGRATION_PLAN.md | 3 +- legendre_convert/legendre_convert.cpp | 50 +++++++++++++++++---------- 2 files changed, 33 insertions(+), 20 deletions(-) diff --git a/ALPS3_MIGRATION_PLAN.md b/ALPS3_MIGRATION_PLAN.md index 383004b..f2a81fe 100644 --- a/ALPS3_MIGRATION_PLAN.md +++ b/ALPS3_MIGRATION_PLAN.md @@ -396,7 +396,8 @@ unchanged. A text-input regression covers trailing blank lines. harmless because the error estimate is seeded from the clock. 3. **Done 2026-09-24:** replace `boost::math::factorial` by a product (only small arguments occur). -4. Replace `boost::math::legendre_p` by the standard three-term recurrence. +4. **Done 2026-09-24:** replace `boost::math::legendre_p` by the standard + three-term recurrence. `std::legendre` is not an option: libc++ (AppleClang) does not implement the C++17 special math functions. diff --git a/legendre_convert/legendre_convert.cpp b/legendre_convert/legendre_convert.cpp index 20827c8..508c08f 100644 --- a/legendre_convert/legendre_convert.cpp +++ b/legendre_convert/legendre_convert.cpp @@ -13,7 +13,6 @@ #include #include #include -#include //needed for Legendre transform #include typedef std::vector vector_type; @@ -21,6 +20,19 @@ typedef std::pair return_type; typedef std::complex Complex ; namespace bmth = boost::math; +double legendre_polynomial(int l, double x){ + if(l == 0) + return 1.0; + double p_lm2 = 1.0; + double p_lm1 = x; + for(int n=2;n<=l;n++){ + const double p_l = ((2*n-1)*x*p_lm1-(n-1)*p_lm2)/n; + p_lm2 = p_lm1; + p_lm1 = p_l; + } + return p_lm1; +} + ///E8 of Boehnke, et al ///high frequency expansion of Gl (see Tl) @@ -82,13 +94,13 @@ double generateGl(const vector_type >au,const vector_type &tau_points, int l,d --ndat_; double dtau = tau_points[1]-tau_points[0]; - gsum+=gtau[0]*bmth::legendre_p(l,2*tau_points[0]/beta-1.0); + gsum+=gtau[0]*legendre_polynomial(l,2*tau_points[0]/beta-1.0); for(int i=1;i Date: Thu, 24 Sep 2026 15:10:14 +0200 Subject: [PATCH 5/6] Address legendre_convert review findings --- legendre_convert/legendre_convert.cpp | 18 +++++++++++++++--- test/regression/MANIFEST.md | 2 +- test/regression/generate.py | 2 +- test/regression/tolerances.json | 2 +- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/legendre_convert/legendre_convert.cpp b/legendre_convert/legendre_convert.cpp index 508c08f..7d38e24 100644 --- a/legendre_convert/legendre_convert.cpp +++ b/legendre_convert/legendre_convert.cpp @@ -9,9 +9,17 @@ * *****************************************************************************/ -#include +#include +#include +#include #include +#include +#include #include +#include +#include +#include +#include #include #include @@ -258,8 +266,12 @@ vector_type generateGaussNoise(vector_type data, vector_type err,std::mt19937 &r const int N = data.size(); vector_type data_noise(N); for(int i=0;i distribution(data[i],err[i]); - data_noise[i] = distribution(rng); + if(err[i] == 0.0) + data_noise[i] = data[i]; + else{ + std::normal_distribution<> distribution(data[i],err[i]); + data_noise[i] = distribution(rng); + } } return data_noise; } diff --git a/test/regression/MANIFEST.md b/test/regression/MANIFEST.md index e95483b..5a3fd03 100644 --- a/test/regression/MANIFEST.md +++ b/test/regression/MANIFEST.md @@ -103,7 +103,7 @@ Rules are matched in order against ':'; the first match applies. | Pattern (case:dataset) | atol | rtol | Why | |---|---|---|---| -| `^legendre_convert:` | 1e-14 | 1e-12 | direct utility regression; allows only rounding-level variation | +| `^legendre_convert_transform:` | 1e-14 | 1e-12 | direct utility regression; allows only rounding-level variation | | `:cli/` | 0 | 0 | command-line output and exit code must not change | | `:log/scalars/(n_singular|max_it_warnings)$` | 0 | 0 | counts; identical in all variants | | `:log/scalars/minimal_chi2$` | 1e-12 | 1e-06 | values below ~1e-12 are rounding noise (observed relative differences up to 1 at 1e-17) | diff --git a/test/regression/generate.py b/test/regression/generate.py index b2fdac7..6add842 100644 --- a/test/regression/generate.py +++ b/test/regression/generate.py @@ -188,7 +188,7 @@ def main(): programs["legendre_convert"] = args.legendre_convert.resolve() missing = sorted({c["program"] for c in selected} - set(programs)) if missing: - ap.error(f"selected cases need --{missing[0]}") + ap.error(f"selected cases need --{missing[0].replace('_', '-')}") results = [] with ThreadPoolExecutor(max_workers=args.jobs) as pool: diff --git a/test/regression/tolerances.json b/test/regression/tolerances.json index b94ae0d..3242ca7 100644 --- a/test/regression/tolerances.json +++ b/test/regression/tolerances.json @@ -1,7 +1,7 @@ { "description": "Rules are matched in order against ':'; the first match applies. A dataset passes if max|a-b| <= atol + rtol * max|reference|. Values are about 10x the largest difference observed between the reference build and four variant builds of the same source (AppleClang -O0, LAPACK SVD, clang 22, GCC 15), rounded up; see PROVENANCE.md. The Legendre example is extremely ill-conditioned (minimal chi2 ~ 1e-17) and gets looser rules for its alpha probabilities and Ng.", "rules": [ - {"pattern": "^legendre_convert:", "atol": 1e-14, "rtol": 1e-12, + {"pattern": "^legendre_convert_transform:", "atol": 1e-14, "rtol": 1e-12, "why": "direct utility regression; allows only rounding-level variation"}, {"pattern": ":cli/", "atol": 0, "rtol": 0, "why": "command-line output and exit code must not change"}, From e74c0efa98ae7c2d42c9b4fb0accc6eafa93e086 Mon Sep 17 00:00:00 2001 From: Emanuel Gull Date: Thu, 24 Sep 2026 15:15:58 +0200 Subject: [PATCH 6/6] Test standard Gaussian noise path --- legendre_convert/gaussian_noise.hpp | 36 +++++++++++++++++++++++++ legendre_convert/legendre_convert.cpp | 17 ++---------- test/CMakeLists.txt | 1 + test/legendreConvertTest.cpp | 38 +++++++++++++++++++++++++++ test/regression/generate.py | 2 +- 5 files changed, 78 insertions(+), 16 deletions(-) create mode 100644 legendre_convert/gaussian_noise.hpp create mode 100644 test/legendreConvertTest.cpp diff --git a/legendre_convert/gaussian_noise.hpp b/legendre_convert/gaussian_noise.hpp new file mode 100644 index 0000000..6c7c746 --- /dev/null +++ b/legendre_convert/gaussian_noise.hpp @@ -0,0 +1,36 @@ +/***************************************************************************** +* +* ALPS Project Applications +* +* Copyright (C) 1998-2016 ALPS Collaboration +* +* ALPS Project: https://alps.comp-phys.org/ +* SPDX-License-Identifier: MIT +* +*****************************************************************************/ + +#pragma once + +#include +#include +#include + +inline std::vector generateGaussNoise(const std::vector& data, + const std::vector& error, + std::mt19937& rng) { + if (data.size() != error.size()) + throw std::invalid_argument("data and error vectors must have equal size"); + + std::vector noisy_data(data.size()); + for (std::size_t i = 0; i < data.size(); ++i) { + if (error[i] < 0.0) + throw std::invalid_argument("Gaussian standard deviation must be nonnegative"); + if (error[i] == 0.0) + noisy_data[i] = data[i]; + else { + std::normal_distribution<> distribution(data[i], error[i]); + noisy_data[i] = distribution(rng); + } + } + return noisy_data; +} diff --git a/legendre_convert/legendre_convert.cpp b/legendre_convert/legendre_convert.cpp index 7d38e24..ae013dc 100644 --- a/legendre_convert/legendre_convert.cpp +++ b/legendre_convert/legendre_convert.cpp @@ -20,6 +20,8 @@ #include #include #include + +#include "gaussian_noise.hpp" #include #include @@ -260,21 +262,6 @@ struct argstruct{ double beta;int l;double tau; std::pair tails; vector_type *tau_points; }; -///genereate a normally distrubted noisy vector. -//i.e. output[i] = normally dist number with mean=data[i] and stddev=err[i] -vector_type generateGaussNoise(vector_type data, vector_type err,std::mt19937 &rng){ - const int N = data.size(); - vector_type data_noise(N); - for(int i=0;i distribution(data[i],err[i]); - data_noise[i] = distribution(rng); - } - } - return data_noise; -} ///Generalized bootstrap routine. Requires the non-linear function to be // f(vector_type v,void *arg) where *arg is most easily a struct return_type bootstrap(double (*f)(vector_type,void*), diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ee2a732..aa259d3 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -28,6 +28,7 @@ set(unit_tests backcontTest default_modelTest gridTest + legendreConvertTest paramFailureTest paramsTest simulationTest) diff --git a/test/legendreConvertTest.cpp b/test/legendreConvertTest.cpp new file mode 100644 index 0000000..7064878 --- /dev/null +++ b/test/legendreConvertTest.cpp @@ -0,0 +1,38 @@ +/***************************************************************************** +* +* ALPS Project Applications +* +* Copyright (C) 1998-2016 ALPS Collaboration +* +* ALPS Project: https://alps.comp-phys.org/ +* SPDX-License-Identifier: MIT +* +*****************************************************************************/ + +#include "../legendre_convert/gaussian_noise.hpp" + +#include + +#include +#include +#include + +TEST(LegendreConvert, ZeroDeviationDoesNotAdvanceGenerator) { + std::mt19937 rng(1234); + const std::mt19937 untouched = rng; + const std::vector data{1.0, -2.0}; + + EXPECT_EQ(generateGaussNoise(data, {0.0, 0.0}, rng), data); + EXPECT_EQ(rng, untouched); +} + +TEST(LegendreConvert, PositiveDeviationUsesDistribution) { + std::mt19937 rng(1234); + const std::mt19937 untouched = rng; + + const auto noisy = generateGaussNoise({1.0}, {0.5}, rng); + + ASSERT_EQ(noisy.size(), 1U); + EXPECT_TRUE(std::isfinite(noisy[0])); + EXPECT_NE(rng, untouched); +} diff --git a/test/regression/generate.py b/test/regression/generate.py index 6add842..8d5007c 100644 --- a/test/regression/generate.py +++ b/test/regression/generate.py @@ -4,7 +4,7 @@ python3 test/regression/generate.py --maxent build/maxent --out /tmp/results \ [--sets fast,targeted,cli] [--cases REGEX] [--components build/dump_components] \ - [--kk build/kk/kk] [--legendre-convert build/legendre_convert/legendre_convert] + [--kk build/kk/kk] [--legendre-convert build/legendre_convert/legendre_convert] \ [--provenance prov.json] [--jobs 8] For every case one HDF5 file /.h5 is written: