This file is committed so any machine's Claude Code session starts from the
same ground truth. Session-local memory (~/.claude/.../memory/) does not
travel between machines — this file does. Keep it updated when a session
establishes something a future session, on any machine, would need.
-
Terminal SOC is always a SOFT constraint, in every algorithm (FilterDDP, centralized Ipopt, tADMM, run_bf): objective
+ gamma * sum_j (B_j^T - B_j^0)^2.gammais fixed per system and never varies with the horizonT. Single source of truth:ddp/examples/power_system/terminal_soc_penalty.jl(ieee123 1.75e4, ieee2522 3.29e4, large10k 420).gammais notC_B. Currently wired into the FilterDDP driver andcentralized_ipopt_matched.jlbehindTERMINAL_SOC_SOFT=1; tADMM and run_bf still need it before their next run. -
C_Bis fixed per system, one number each (user, 2026-09-26), sized so each battery's price-driven swing is 3x the system's MEDIAN battery rating:C_B = (c_max - cbar) / (2 * 3 * median P_rated)= ieee123 1.0e-3, med2522 1.9e-3, large10k 2.4e-5. Same single source file as gamma (CB_BY_SYSTEM), selected in both FilterDDP and Ipopt byREDUCED_CB=system. The earlier uniformC_B = 1e-3left large10k's (417 kW median) batteries mostly idle (5% power use, 30% of the energy window); with these values every system uses 100% of its energy window. Derivation and checks:ddp/notes/IPOPT_WITHOUT_CB.md.C_Bnever varies withT. The diagonal Hessian relies onC_B > 0: atC_B = 0diagonal-Hessian FilterDDP fails large10k T=6 (line search, iteration 2) and med2522 T=96, while the exact Hessian converges (large10k T=6, 108 iterations, no regularization). Never pairFILTERDDP_DIAG_HESSIAN=1withC_B = 0. (Those failures were measured before the 2026-10-05 configuration. In it, both cases reach near-optimality with the diagonal Hessian atC_B = 0: large10k T=6 in 17 iterations, med2522 T=96 in 55. The rule stands until the user lifts it.) -
Solver timing comparisons only on a provably identical problem: same exported instance,
C_B,gammaand profile, with objective agreement checked before any timing is quoted. The paper's older centralized Ipopt sweep (ddp/results/centralized_ipopt/) is on a DIFFERENT instance family (C_B ~ 8.8e-8, old price sampling, free terminal SOC) -- do not race against it. -
Near-optimality primal infeasibility thresholds are per-system (user, 2026-09-23). FilterDDP "time to near-optimality" (Table V in the TPEC paper) is the first iteration with objective within 0.5% of Ipopt AND primal_inf below the system threshold: ieee123
1e-6, ieee25221e-5, large10k1e-5. Single source of truth:NEAR_OPT_PRIMAL_BY_SYSTEMinddp/examples/power_system/terminal_soc_penalty.jl. The race script (run_matched_ipopt_race.sh) reads matching values. Do not use a uniform threshold across systems. large10k was1e-4until 2026-10-11. The user moved it to1e-5after seeing the clean batch re-read at both ("1e-5 is the sweet spot";1e-4a bit too loose,5e-5too arbitrary): Ipopt's own infeasibility falls below1e-5only near the end of its solve, the diagonal FilterDDP iterate at the 0.5% point is already below it fromT=24up. Every large10k near-optimality figure dated before that, in the notes and in the TPEC paper (results table, screening-ablation column), is at1e-4. Runs that stopped at near-optimality cannot be re-read at1e-5; runs to tolerance (the clean batch) can (clean_hp_table.py). -
Every timed FilterDDP run must enable full per-iteration logging (user, 2026-09-23). Set
FILTERDDP_TIMING_DIAGNOSTIC=1andFILTERDDP_FEASIBILITY_DIAGNOSTIC=1. This emits per-iteration: objective, primal_inf, dual_inf, complementarity (cs_inf), barrier parameter (mu), regularisation, step size, backtracks, and per-phase wall time (backward_s, forward_s, total_s). The race script extracts a trace CSV (extract_filterddp_feasibility_trace.jl) and the NO-point summary (near_opt_from_logs.jl) post-hoc from these logs. Without these env vars the run is unreportable — you cannot reconstruct the NO crossing point. -
Report solve time, not compile time (user, 2026-10-05). Julia compiles each method on first use: 15-30 s, all inside FilterDDP's first iteration (14.7 of the 17.9 s once reported for ieee123
T=6). Timed FilterDDP runs setFILTERDDP_WARMUP=fullon the Table II cells: the whole solve once, silently, then the timed solve from the same start, same iterates. (A few warm-up iterations are not enough: they leave about 1.7 s of compilation in code reached later in the solve.) At horizons of 192 or more a full extra solve is too long; useFILTERDDP_WARMUP=8there. Every FilterDDP time measured before 2026-10-05 includes compilation. -
Screened model and power-flow start, for both solvers (2026-10-05, see
ddp/notes/CONSTRAINT_SCREENING.md). The substation voltage is fixed at exactly its upper limit, which stopped FilterDDP from ever taking a full step; every iteration count measured before 2026-10-05 carries that throttle. Current configuration:FILTERDDP_SCREEN=substation,vupper,ell,psubs,FILTERDDP_LOADFLOW_START=1,FILTERDDP_AFFINE_LINESEARCH=1. The reductions are exact (same optimum), but they and the start also speed Ipopt up (53 to 12 iterations on large10kT=6), so any comparison gives Ipopt the same:IPOPT_SCREEN=<same list>,IPOPT_LOADFLOW_START=1. -
large10k had no PV real power until 2026-10-05.
envs/tadmm/parse_opendss.jlread each PV rating asPVsystems.kW(), the unit's present output. large10k's units carryDaily=LoadShapePVDefault, so all 1,022 read as 0 kW (350 kW each, 357.7 MW in total); ieee123 and ieee2522 have no daily shape and were unaffected. Found by the OpenDSS check (ddp/examples/power_system/opendss_check.jl), fixed to readPmpp. The large10k instances were re-exported; the old ones are kept locally asnetwork_data_large10kC_1ph_T*_periodic_nopv.jls. Every large10k result dated before this, for either solver, is on the PV-less system (still a matched comparison); logs on the corrected instance carry the tagpv, and Ipopt references must come from it. The MSOPF parsers inenvs/multi_poistill use the same call. Standing practice (user): check a dispatch against OpenDSS before trusting an instance or a result. -
Logs over 100 MB cannot be pushed to GitHub. Per-iteration logs at horizons of several hundred steps reach 75-140 MB: commit them gzipped, and check after every push that the branch is not still ahead of the remote.
-
Exact rewrites of 2026-10-06, and the parallel-in-time count (
ddp/notes/PARALLEL_IN_TIME.md, Section 9).FILTERDDP_LEAN_VALUE=1(identical iterates) andFILTERDDP_HALF_BLOCK=1(battery block at half size; iterates agree to rounding, check withcompare_runs.py) make the sequential run 43-44% faster on large10k and 24-27% on med2522 than the 2026-10-05 configuration. They are in the TPEC paper since 2026-10-06 (its results table and Section "Cost of One Iteration") but not yet in the standard run scripts other thanrun_parsim.sh; for ieee123 (UMFPACK, no tree solver) passFILTERDDP_LEAN_VALUE=1torun_blocked_solve_fullrun.sh.FILTERDDP_PARSIM=1times each stage as what needs nothing fromt+1/ what needs the value function / what follows it, andparsim_from_log.pycounts one worker per period (the slowest period for the independent parts). The user accepts that simulated count ("like my tADMM Ipopt solve"); always state that it assumes one core per period, no communication, and that Ipopt is a single process. -
NO_COLORin the environment defeats the warm-up (found 2026-10-06). Julia then wraps stdout in an IOContext and every print is compiled again in the timed solve (0.7 s; 1.5-2.3 s with the PARSIM lines). Processes started from PowerShell carry it.run_parsim.shandrun_blocked_solve_fullrun.shunset it; the driver printsWARMUP_STREAM_MISMATCHwhen it happens. Any new timed launcher must unsetNO_COLORandFORCE_COLORtoo. Do not edit solver sources while a queued batch is running: each run loads them afresh. -
Scope of interest (user, 2026-10-06). Methods must help every test system: no speedup that exploits the topology of one system and does nothing for another (large10k is, to
1e-7, 102 independent feeders; that is a caveat about the test system, not a method to build). Horizons of interest are the practical ones,T=6toT=96(up to a few hundred at most): med2522T=1152andT=1536were cancelled and are not to be requeued. A horizon must mean something (user, 2026-10-11): one day at a standard step. BeyondT=96(15 minutes, the standing goal-post) the points areT=144(10 minutes) andT=288(5 minutes), notT=192. Several days is a possible later step, once a solve is cheap enough. Naming them is not a request to run them: the user stopped queuedT=288runs the same day ("I only want 144", for med2522, large10k optional). Ask before anyT=288run. -
The OpenDSS check is part of every run (user, 2026-10-11). Stop the timer, keep the dispatch, hand it to OpenDSS, log the discrepancy, then start the next run; there is no timing conflict in that order.
run_parsim.shdoes it by default (OPENDSS_CHECK=1); any new launcher must, the centralized ones included ("everything goes into OpenDSS"). Never setFILTERDDP_SKIP_SOLUTION_WRITE=1in a run whose result may be reported. -
Stopping rules (user, 2026-10-10). Near-optimality always means the objective criterion AND the primal-infeasibility criterion together. FilterDDP runs go to a tolerance of
1e-5and centralized runs to1e-6, with the objective and the infeasibility of every iteration logged (IPOPT_TRACE=1for Ipopt), and the reported criterion is chosen afterwards, the same for both solvers. Two primal infeasibilities exist and differ a great deal: the internal one (an inequality counted through its slack: FilterDDP'sprimal_inf, Ipopt's callback) and the as-posed one (Ipopt's iteration table,FILTERDDP_ORIGINAL_PRIMAL). Never compare one solver's internal figure with the other's as-posed one. -
Power plan. The 309 PC under High performance is 14-24% faster per iteration than under Balanced (same runs, 2026-10-05/06 against 10-09; indicative). Every time in the TPEC table up to 2026-10-10 was measured under Balanced, for both solvers; the clean batch of 2026-10-11 (
run_clean_hp_batch.sh, logs taggedhp) is under High performance. Never mix the two in one comparison; record the plan with the run. -
Exact Hessian and the pairwise sweep (2026-10-10/11,
ddp/notes/PARALLEL_IN_TIME.md, Section 10).FILTERDDP_LEAN_EXACT=1(withFILTERDDP_HALF_BLOCK=1, noFILTERDDP_DIAG_HESSIAN) builds the halved battery block before the sweep: same iterates as the plain exact path, and its step B costs about what the diagonal path's does.pint_scan.jl(FILTERDDP_PINT_SCAN=1) redoes the value recursion by pairwise combination of the periods (Sarkka and Garcia-Fernandez, 2023): exact, slower atT=6, 1.7-2.0 times faster atT=24and 4.6-5.1 times atT=96on step B, as a count. Since the night of 2026-10-11 a solve uses it (paired_pass.jl, bullet below), and the TPEC paper's counted column is the exact Hessian with the pairwise sweep; the diagonal Hessian stays the faster sequential arrangement on the two larger feeders. -
Gurobi works on the 309 PC (Gurobi 13, licence to 2027-09-22) and is run for reference (user, 2026-10-06 and 2026-10-11). It needs
vandellnonnegative to recognise the cone, so its reduced model keeps those two and drops the other screened limits; its barrier method takes no starting point for a conic problem (Gurobi documentation, "Warm Start Options"), so the power-flow start has no effect on it. -
Night of 2026-10-11 (
ddp/notes/PARALLEL_IN_TIME.md, Section 11; branchddp-oct10-new-tests). Three things a later session should know:FILTERDDP_REUSE_BARRIER=1: the backward pass thatsolve!repeats after lowering the barrier parameter keeps every stage's factor (no stage matrix depends on that parameter). Identical iterates, checked on all three systems with both Hessians. Use it in timed runs from now on; logs carry the tagreuse.- Barrier schedule and hybrid Hessian, 132 untimed runs: Ipopt's schedule
(
FILTERDDP_MU_INIT=0.1,_THETA_MU=1.5) cuts the iterations to tolerance (252 to 187 over six cases with the diagonal Hessian) but is mixed at the 0.5% rule and costs iterations on large10k; the hybrid (FILTERDDP_HYBRID_MU) gains nothing. The default schedule stays until the user decides otherwise. FILTERDDP_THREADED=1(threaded_pass.jl): the backward and forward passes with one task per period, same iterates as the sequential run (bit for bit backward, to rounding forward). Correctness only: the user does not want parallel timings from the 309 PC. It is the code for a machine with a core per period; a Kamiak account was requested on 2026-10-11 (ticket KSD-7186, PI Dubey).
-
The pairwise sweep in the solve, and the collector in the count (2026-10-11 night;
ddp/notes/PARALLEL_IN_TIME.md, Section 12).paired_pass.jldoes the battery sweep by pairs inside the solve: same iterations and objective as the ordinary exact solve on all twelve cases. Launcher:bash ddp/results/paired_pass/run_paired.sh <system> <T> count <tag>(OpenDSS check included;PAIRED_ROUNDS=0runs the ordinary sweep in the same pass for a count on the same timing;PAIRED_WARMUP=full, 8 on large10kT=96). The driver has no switch of its own for it yet.- The count charges garbage collection per worker (user, 2026-10-11:
"our default assumption", and the paper must say so). Julia's collector
takes 17-22% of a run; its pauses used to land inside one stage's time
and make it the slowest. Counting runs now time each stage without the
pauses in it and print the collector's time (
FILTERDDP_PARSIM_GCNET=1, set by the launcher), and the scripts give each worker1/Tof it;clean_hp_table.py --gc=sequentialgives the other reading. Logs before this (the clean batch, tagscount_hp_tol1e5andinplace_hp_tol1e5) are on the old timing: never put their counts beside the new ones as like for like. Always state the three assumptions: one core per period, no time for passing data, a memory per worker; and that Ipopt is one process. A real parallel run should be one process per period, not threads in one program. - Result at the 0.5% rule (same at 0.1%), Ipopt / DDP counted, seconds:
med2522 13.3 / 4.2 (
T=24), 67.8 / 8.4 (T=96), 110.2 / 14.4 (T=144); large10k 21.9 / 12.4, 44.4 / 15.8 (T=48), 101.4 / 22.6 (T=96). AtT=6: level on med2522, half Ipopt's speed on large10k. With every collection in sequence only large10kT=48andT=96stay ahead. One process runs the pairwise arrangement slower than the ordinary sweep. - Do not retry "run the collector less often": held off for a whole pass and run once it costs the same (its cost follows the volume allocated), and the allocation has no single source.
- Longer horizons (user's go, 2026-10-12): med2522
T=288counts 26.4 s against 255.2 s for Ipopt (9.7 times). large10kT=144counts 80.6 s against 138.9 s, but the PC (31.7 GB) runs short of memory there and the count carries the stalls (about 45 s without them, an estimate): say so wherever that figure appears (an asterisk in the paper's table, explained in the text). A clean rerun needs the other applications closed; the user parked it on 2026-10-12 until there is access to a parallel machine. large10kT=288cannot run on this PC. Warm-up 12 for med2522T=288, 8 for large10kT=144. - ieee123
T=24needs tolerance 1e-6 (PAIRED_TOL) to meet its 1e-6 infeasibility threshold.
-
The paper targets IEEE Transactions on Power Systems (user, 2026-10-12; it was drafted as an Industry Applications paper). Its GitHub repository was renamed on 2026-10-13 to
Realife-Brahmin/TPWRS-2027-Sparse-Parallel-DDP-for-MPOPF, and the local folder todocuments_general/TPWRS-2027-Sparse-Parallel-DDP-for-MPOPF(it wasTPEC-2027-DDP-MPOPF: "the TPEC repo" and "the TPEC paper" in older notes mean this one). Its two linked worktrees under.codex/worktrees/were repaired after the move. PES rules, from the PES authors' page: 10 pages at submission (returned without review above that), 12 before mandatory overlength charges; abstract of about 150-200 words in one paragraph with no abbreviations, references or equations; three or four keywords; AI-generated content must be disclosed in the Acknowledgments (system, sections, level of use). As of that date the abstract (111 words, with abbreviations, written before the results), the keywords and the acknowledgment were still to do, and are the user's to word. The algorithm's name is Sparse Parallel DDP (user, 2026-10-13), in the title and defined in the introduction; "DDP" stays the shorthand in the body and tables. The user's wishes for the front matter: an abstract in the conventional order (problem, structure, "this paper presents", the strategies in a sentence each, tests, comparison), horizons given as a day at a resolution (288 periods = 5 minutes, 144 = 10 minutes), the method not presented as resting on the network being radial (say tree-like or sparse; the elimination needs only that loops stay in the kept block, which is not implemented), and index terms that are specific (elimination tree, parallel prefix scan, Riccati recursion, Schur complement), in alphabetical order. Two claims the user floated are not supported by the runs and must stay out unless measured: that the method uses less memory than the centralized solver (on one PC it used more: 17-20 GB against 13.6 GB at large10kT=144), and that it solves cases the centralized solver cannot (Ipopt with MA57 solved every case run).
Any branch named in this file may already be stale. It records what was
current the last time someone edited this line, not what is current now. As of
2026-09-17 this section said ddp-understanding-sep14 while the remote's newest
branch was ddp-understanding-sep15 — assume that kind of drift by default.
Always resolve the branch from the remote before starting work:
git fetch origin --prune
git for-each-ref --sort=-committerdate refs/remotes/origin \
--format='%(committerdate:short) %(refname:short)'Then pick the branch whose name matches the task you were given, taking the latest date among those — not simply the newest branch in the list. The date suffix breaks ties within a workstream; it does not make an unrelated workstream's branch the right base.
Workstreams run on parallel dated branches and are expected to coexist.
The three use cases in this repo (tADMM, DDP, and MSOPF — the environment still
on disk as envs/multi_poi/, being renamed MultiSourceOPF) share the same
underlying optimization but are separate lines of work with separate branches.
DDP work belongs on its own dated branch (ddp-understanding-<date>,
ddp-hessian-rewrites-<date>, ...); MSOPF work belongs on its own, e.g.
msopf-initial-testing-sep17. Do not retarget MSOPF work onto the current DDP
branch (or vice versa) merely because that branch is newest.
These branches are not expected to conflict: MSOPF's task is building a working OpenDSS-backed workflow, not running heavy optimization, so it touches largely different files. If you do find yourself resolving conflicts between two workstream branches, stop and confirm the base branch was chosen correctly before continuing.
For genuinely new work, cut a new dated branch rather than reusing an existing one, and name it for the workstream it serves.
- Setup, drivers and decks:
envs/multi_poi/README.md. The full-angle drivers run inenvs/tadmm(tracked Manifest; checked from a fresh clone), notenvs/multi_poi. - The paper is the separate
PESGM2027_Multi-Source-Multi-Period-OPFrepo. Its Section II documentssolve_full_angleinenvs/multi_poi/full_angle_pf.jl, so keep the two in sync. Do not confuse it with the rejected PESGM2026 repo. - Decks
ieee123_5poi_1phandsmall2poi_1phcarry a realistic source impedance, Zs = 0.032 + j0.799 ohm (cited in the deck comments).ideal_sources = truegives the old stiff sources. - Battery model: one variable
P_Bper battery, lossless, with an optionalC_B * P_B^2cycling cost (the user's standard). Feasibility means the network equations, no-backflow (P_Subs >= 0, the key constraint) and the 0.95-1.05 pu band on buses not set by a substation.
Since 2026-09-15 the DDP solver is our own MIT fork of FilterDDP.jl, committed
as source, named DDP4OPF with a fresh UUID (005a218a-...). using DDP4OPF
everywhere; FilterDDP is no longer resolvable in envs/ddp2026. Mingda Xu's
MIT notice is retained in its LICENSE, and NOTICE.md records the upstream
repo, the fork point 513a104, and what changed. Credit the original papers.
Why it was forked rather than kept as a clone plus patches: the working solver
existed only as uncommitted edits in a gitignored clone, and the documented patch
recipe no longer rebuilt it (3 of 9 patches failed on a clean 513a104; the
result differed in 4 source files). The fork was verified bit-identical to
that clone -- 0 of 2526 full-space and 0 of 459 reduced-space solution entries
differed on ieee123 T=3.
- A new name and UUID were required, not cosmetic: FilterDDP is registered in the Julia General registry, and Julia identifies packages by UUID.
ddp/patches/is history, not a build recipe. Do not try to rebuild from it.- Environment variables still use the
FILTERDDP_prefix on purpose: sweep and queue scripts set them. Renaming them is a separate decision. ddp/external/FilterDDP.jl(gitignored) is now only needed to reproduce the authors' own shipped example (Stage 3).- Historical notes in
ddp/notes/still citeddp/external/FilterDDP.jlpaths and line numbers. They record the clone as it was at the time; leave them.
That sweep is finished across all three systems and all matrix horizons, with
the TPEC table/caption/PDF updated. The procedure below is kept for reference. Reconstruct the IAS-style
centralized C (s) column with fresh JuMP--IPOPT runs by following
ddp/notes/CENTRALIZED_IPOPT_TIMING_SWEEP.md. Use
scripts/run_centralized_ipopt_case.ps1, one case at a time. A case is not
finished until its validated row and raw log are pushed here and the matching
TPEC table/PDF update is pushed to the TPEC repository. Resume from the first
incomplete row; never replace missing data with an older Gurobi timing.
Naming: the user's method is DIFFERENTIAL Dynamic Programming. It is never
"Distributed." Corrected throughout on 2026-08-14, including the two source
files that originally carried the wrong expansion
(envs/ddp/tex/ddp_copperplate_formulation.tex,
envs/ddp/root_level/ddp_copperplate.jl). An earlier version of this file
recorded "Distributed" as a deliberate word choice; that was wrong. Do not
reintroduce it anywhere, and do not describe the two codebases as different
algorithms that merely share an initialism — they are both DDP, differing in
order. (Unrelated: "Distributed Energy Resources" in envs/multi_poi/ and
envs/tadmm/ is correct and must be left alone; and the repo name's "Dist"
means distribution networks.)
-
ddp/(repo root) is the FilterDDP evaluation: a reproducibility study of the authors' solver (github.com/mingu6/FilterDDP.jl, arXiv 2504.08278 / 2606.01487), done 2026-08-03 to present. Full status, findings, and the MPOPF-fit conclusion are in ddp/README_FILTERDDP_EXPERIMENT.md. The paper is atddp/resources/Xu_2026_FilterDDP.pdf(and a duplicate atenvs/ddp/resources/Xu_2026_FilterDDP.pdf, see manifests below). -
envs/ddp/is the user's own, independently-derived first-order DDP scheme for copper-plate MPOPF (committed 2025-11-12, predates any Claude involvement — verified by git blame, not AI-authored). A clean minimal restatement of it lives at ddp/examples/power_system/user_ddp.jl.
The difference is the order of the cost-to-go model, not the family:
envs/ddp/ (user's own) |
ddp/ (FilterDDP, the paper) |
|
|---|---|---|
| Backward information | Passes μ[t], the dynamics-constraint dual, backward one stage per outer forward sweep (envs/ddp/root_level/ddp_copperplate.jl:539-550, mu_prev/mu_coupling) |
Backward pass sweeps t=N→1 within one iteration, building both V_x and V_xx (Riccati recursion, backward_pass.jl in the FilterDDP clone) |
| Order of approximation | First-order only: the coupling term μ[t+1]·(B[t+1]−B[t]+Δt·P_B[t+1]) is linear in B[t] — no curvature crosses a stage boundary |
Second-order: full local quadratic model of the cost-to-go |
| Per-stage solve | Calls an external solver (Gurobi/Ipopt) per stage per sweep | In-house sparse nu×nu KKT solve (UMFPACK) inside the backward pass, with filter line-search globalization. An earlier version of this row said "dense"; measured at large10k a dense factorisation would need ~540 s and 22 GB per stage against 2.25 s actual |
| Propagation speed | Information from stage T reaches stage 1 after roughly T outer iterations (one stage per sweep) |
Full-horizon propagation in one backward sweep per Newton-type iteration |
Important nuance established 2026-08-05: μ[t] in the user's method is
legitimately the same object as V_x[t] (the costate / value-gradient) —
this is not a naive approach, it's a genuine first-order DDP. The gap
is the missing curvature (V_xx) and the cross-iteration staleness, not the
absence of backward-looking information altogether.
Measured 2026-08-14 on the shared T = 6 instance, via user_ddp.jl:
the first-order scheme's fixed point is correct — fed the true μ* and B*,
one sweep reproduces the centralized optimum to 1.7e-08 — but it does not
converge to it from a cold start, settling into a period-two limit cycle
2.1e-03 short in objective and 121 kW out in dispatch. Damping shrinks the
cycle without closing it (a = 0.1 still 3.2e-05 short after 2000 sweeps),
because started at the optimum the sweep returns μ off by 9.2e-04: the
optimum is not exactly a fixed point of the linearised map. FilterDDP reaches
2.2e-10 on the same instance in 16 iterations. That is the V_xx gap,
quantified.
Since 2026-08-07 there is one environment for all DDP work — FilterDDP, the
centralized JuMP/Ipopt reference, and any further formulation:
envs/ddp2026 (README). FilterDDP and JuMP coexist
without conflict. Stages 5, 6 and 8 all reproduce under it.
envs/ddp/Project.toml(2025-11) is superseded. ItsPlots/Crayons/Revisedeps existed because verification then meant a human reading formatted output. Treatenvs/ddp/as a read-only reference for what the user's own algorithm did — not expected to be run again.ddp/env(the FilterDDP-only env) was removed 2026-08-07. ItsManifestwas never tracked, so it preserved nothingenvs/ddp2026/Project.tomldoesn't, and Stages 5/6 reproduce identically under the new env. It's in git history if ever needed.- Pkg strips all comments from
Project.tomlon everyadd/resolve, so put rationale in a README beside it, never in the file. - Julia via the Bash tool, not PowerShell: PowerShell mangles quotes in
julia -e '...'and has silently corruptedProject.tomlthis way.
P_Subs^t has no upper bound — only P_Subs^t ≥ 0 (no export upstream).
This is the latest formulation, confirmed by the user 2026-08-07. Older notes
referring to an active Psub[2] ≤ 1.35 or ≤ 1.45 are stale; the scripts and
logs were already correct and the experiment README has been fixed to match.
The ddp/paper/ write-up (including copper_plate_model.tex) was deleted
2026-09-08 — it encoded a superseded formulation. The authoritative
formulation now lives in the TPEC repo, not here. The modeling rationale
that section used to carry (why no η, why the terminal target is a penalty,
why C_B = 0.5 here vs. ≈10⁻⁶·min c^t in the tADMM paper) survives only in
ddp/README_FILTERDDP_EXPERIMENT.md.
Instance data is now the tADMM profiles (changed 2026-08-07 at the user's
request). Demand and price come from envs/tadmm/root_level/config.jl, shared via
ddp/examples/power_system/tadmm_profiles.jl. Three things to know:
- They resample with T —
tadmm_cost(3)is NOTtadmm_cost(6)[1:3]. Never slice a fixed vector. - Committed results are T = 6 only (T = 3 removed 2026-08-07 at the user's
request; regenerating another
Twhen analysis needs it is expected). But inspect the price before adopting a newT: at T = 3 the samples land wheresinvanishes (0, π, 2π), so the price comes out constant and the instance carries no arbitrage signal at all. That, plus binding bounds only a few kW wide, made T = 3 both the weakest benchmark and the worst behaved numerically (26 iterations, trajectory agreement only1.9e-05). - This closed the earlier collinearity concern:
rwent from0.9968to0.644at T = 6, thanks to the−0.8rad phase offset on the load.
C_B = 0.05 since 2026-08-07 (was 0.5). This is the COPPER-PLATE value and
does not apply to the network cases: the exported network_data_*.jls files
carry C_B = 1.4e-07 with dt = 8 (the tADMM regime). Established 2026-09-15 --
the difference is not cosmetic, see the reduced-space section below. C_B sets
how far the battery moves:
P_B^k = (c^k - 2wΔt·s)/(2C_B), so the swing scales as 1/C_B — the bounds are not
the knob. At 0.05 the T = 6 battery swings ±570 kW against a 1623-1998 kW load and
cycles B from 2000 down to 1070 kWh: 46% depth of discharge, matching the
ieee123C_1ph battery/load ratio of 44%. cond(Q) = 601, so the closed-form and
active-set references stay exact to ~1e-14.
Still not tADMM's own C_B = 1e-6·min(c) ≈ 8e-8: there cond(Q) ≈ 3.6e8 and the
battery goes purely bound-limited (±340 GW absent bounds) — the tADMM regime, but
useless as a precision reference. An earlier note that this makes Q rank-1 was an
overstatement: Q = 2C_B Δt I + 2w Δt² 11ᵀ is PD for any C_B > 0, rank-1 only in
the limit.
Plot battery quantities in kW and kWh, never p.u. (user preference, 2026-08-07).
Base is tADMM's kVA_B = 1000: 1 p.u. = 1000 kW, 1 p.u.h = 1000 kWh. The model and
Table I stay in p.u.; the figures convert. Reference asset sizes, for sanity checks:
ads10A_1ph 87 kW load / 4.7 kW batt; ieee123C_1ph 1163 kW / 507 kW / 2027 kWh;
ieee123_5poi_1ph 1163 kW / 1318 kW / 5273 kWh. SOC runs 30%-95% with B_0 at 62.5%.
Figures are generated, never hand-written was the rule for the (now-deleted)
ddp/paper/figures/ — make_figure_data.jl read the verified centralized
reference and emitted balance.csv, schedule_interval.csv, schedule_soc.csv
for the .tex files to read. Keep the same discipline (generated data, no
inlined coordinates) if/when figures are rebuilt against the TPEC repo's
formulation.
Full write-up and raw data:
ddp/notes/REDUCED_SPACE_INNER_OPF_FEASIBILITY.md,
ddp/results/reduced_space/. The reduced-space work did not modify the solver
(now ddp/DDP4OPF.jl) -- the OCP is assembled from hand-written closures, which
is forced anyway since an Ipopt solve is not automatically differentiable.
The network can be eliminated exactly: FilterDDP optimises battery quantities
only while an inner single-period OPF recovers every network variable, with the
real-power balance duals supplying dPhi/dP_B for free. Verified against the
stored full-space FilterDDP solutions to 6.0e-09 (ieee123) and 1.3e-08
(ieee2522) relative objective.
RETRACTED 2026-09-15 -- read before trusting the table below. Every
T = 3instance has exactly zero price spread: the profile generator sampledsinat0, pi, 2pi. With no arbitrage signal the batteries barely move (5% of rating at large10k), so the table measures an idle-battery problem. It is a valid timing on that instance and not evidence the decomposition works on a real scheduling problem. On the first instance with a real price signal (ieee2522T = 12, 147% spread, batteries up to 97% of rating) the reduced method fails to converge while full space converges. Why is still open. UsePROFILE_PERIODIC=1exports (118% spread atT = 3); the exporter now warns on flat profiles. Full account:ddp/notes/FINDINGS_2026-09-14_15.md.
Measured crossover at T = 3, C_B = 1e-3, both formulations re-run at the
same C_B (degenerate instances -- see retraction above):
| full-space | reduced (low-rank) | outcome | |
|---|---|---|---|
ieee123 (nu 791 -> 102) |
14.4 s / 46 it | 21.7 s / 26 it | 1.5x slower |
ieee2522 (nu 13358 -> 500) |
107.3 s / 56 it | 107.1 s / 33 it | parity |
large10k (nu 54665 -> 2040) |
1658.2 s / 100 it | 802.5 s / 13 it | 2.07x faster |
That was read as "the decomposition is a LARGE-system technique"; given the retraction, that conclusion is unsupported until it is re-measured on non-degenerate instances. Outer iteration counts run the other way with size (full-space 46/56/100, reduced 26/33/13), and at large10k inner solves are only 25% of wall -- the bottleneck has moved to FilterDDP's own outer cost.
C_B decides whether any of this works. It enters the stage Hessian only as
a perfectly-conditioned diagonal 2*C_B*S^2*dt, and on ieee2522 d2Phi spans
-0.148 .. 21194. At the exported C_B = 1.4e-07 that diagonal is 2.24, so
there is effectively no damping (cond ~10131) and EVERY cheap curvature model
fails -- battery-term-only, low-rank Nystrom, and floored Nystrom all stall.
Only an exact per-stage Hessian converges, and it is too expensive to win. At
C_B = 1e-3 the diagonal is 16000 (cond 2.32) and the low-rank Hessian
converges, which is what produces the table above. Size C_B from measurement:
2*C_B*S^2*dt > -lambda_min(d2Phi) is mandatory for positive definiteness, and
~ lambda_max/kappa sets conditioning; lambda_max costs ~10 inner solves by
power iteration since H*d is one solve.
Two further results worth not re-deriving: the battery-power box is not
recourse-feasible in general (true on ieee123, false on ieee2522 -- 8 of 111
dispatches rejected, all undervoltage), and F_t is horizon-independent, so T
only selects which load/PV snapshots get tested.
FilterDDP is cross-checked against a centralized JuMP/Ipopt solve of
eq:cp_all on all six T = 6 cases (Stage 8 of the experiment README): exact on
the base instance, worst objective gap 2.2e-10 and worst trajectory gap
6.9e-09 with bounds, and Ipopt independently certifies the 6g bound set
infeasible. The base instance carries three mutually independent references
(closed form, dense KKT, Ipopt). Stage 9 records the per-iteration trace — 17
iterations, regularisation never firing. Do not redo any of this.
Not verified, so don't overclaim: anything with a network (no LinDistFlow, no
BFM); any horizon other than T = 6; and the user's own DDP algorithm, which
has not been compared at all — that is the pending task below.
Blocked as of 2026-09-08: this task's shared reference point,
ddp/paper/sections/copper_plate_model.tex, was deleted along with the rest
of ddp/paper/ — that formulation is superseded and the authoritative one now
lives in the TPEC repo. Before restarting this task, get the equivalent
problem statement from the TPEC repo and re-approve it as the shared reference
point; do not resurrect the deleted file from git history as a substitute.
Write a side-by-side workflow comparison — the user's exact DDP algorithm
vs. FilterDDP's algorithm — both grounded in the exact problem statement of
the "dummy paper" (previously ddp/paper/sections/copper_plate_model.tex,
see blocker above). Requirements:
- Use the user's own notation throughout:
P_Subs^t,P_B^t,B^t,c^t,C_B,p_L^t,w,B_0,P_B^{min/max},B^{min/max}(fromcopper_plate_model.tex), plusμ[t],λ_Bmin[t],λ_Bmax[t](fromenvs/ddp/tex/ddp_copperplate_formulation.tex). No unqualified generic control-theory notation (L,l,Q,V_x,V_xxunless explicitly mapped to one of the user's own symbols first). - Every dual/auxiliary variable that plays the same role in both workflows
should be given the same symbol in both descriptions, with the
correspondence stated explicitly (e.g.
μ[t] ↔ V_x[t], the box-constraint dualsλ_Bmin[t]/λ_Bmax[t]vs. FilterDDP's interior-point bound multipliers). - Where a concept exists in FilterDDP but has no analog in the user's method
(e.g.
V_xx/curvature), say so explicitly rather than inventing a correspondence.
Every resources/ folder (ddp/resources/, envs/ddp/resources/,
envs/multi_poi/resources/) has a MANIFEST.txt (filename | url | description). Run bash scripts/fetch_resources.sh from repo root to
fetch anything missing — safe to re-run, present files are left alone. Blank
url fields mean the source isn't tracked down yet; fill in when found.