*! _gvar_stability 1.0.1  21aug2026
*! gvar stability -- structural stability battery for every country-model
*! equation.
*! Author: Merwan Roudane
*! merwanroudane920@gmail.com
*! https://github.com/merwanroudane
*
* Step -> source map
*   PKsup, PKmsq  Ploberger & Kramer (1992) maximal OLS-CUSUM and its
*                 mean-square variant          <- Toolbox kraplob.m
*   Nyblom        Nyblom (1989) LM against a
*                 non-stationary alternative,
*                 plus its robust version      <- Toolbox nyblom.m
*   QLR           Quandt likelihood ratio, sup-F
*   MW            mean-F
*   APW           Andrews & Ploberger exp-F
*                 and heteroskedasticity-robust
*                 versions of all three        <- Toolbox schow.m
*   break date    argmax of the sequential Chow <- Toolbox schow.m (maxobs)
*
* The Toolbox obtains critical values by bootstrapping the solved GVAR
* (bootstrap_GVAR_ss.m); that requires gvar irf, reps() and is offered there.

program define _gvar_stability, rclass
    version 14.0

    syntax [,                                   ///
        CCUT(real 0.15)                         ///
        TESTs(string)                           ///
        REPS(integer 0)                         ///
        SHUFFLE                                 ///
        SHRINKDraw                              ///
        LAMDraw(real -1)                        ///
        VCOV(string)                            ///
        SHRINK                                  ///
        LAMbda(real -1)                         ///
        DETail                                  ///
        noSUMmary                               ///
        SAVing(name)                            ///
        SAVECV(name)                            ///
        GRAPH                                   ///
        EQuations(string)                       ///
        TOP(integer 6)                          ///
        NAME(string)                            ///
        EFP(string)                             ///
        HFRAC(real 0.15)                        ///
        ALL                                     ///
    ]

    _gvar_require estimate

    * ---- the empirical fluctuation processes, a separate battery ------------
    if ("`efp'" != "" | "`all'" != "") {
        if (`hfrac' <= 0 | `hfrac' >= 1) {
            di as err "hfrac() must lie strictly between 0 and 1"
            exit 198
        }
        _gvar_shrinkopt 0 0 "`vcov'" "`shrink'" "`lambda'"
        local shf 0
        if ("`shuffle'" != "") local shf 1
        local dgs 0
        local dgl .
        if ("`shrinkdraw'" != "") local dgs 1
        if ("`lamdraw'" != "" & "`lamdraw'" != "-1") {
            local dgs 1
            local dgl `lamdraw'
        }
        _gvar_stab_efp "`efp'" `hfrac' `reps' `shf' `r(vmeth)' `r(vexcl)' ///
                       `r(shr)' "`r(lam)'" `dgs' "`dgl'" "`summary'" ///
                       "`graph'" "`equations'" `top' "`name'" "`all'"
        return add
        exit
    }

    if (`ccut' <= 0 | `ccut' >= 0.5) {
        di as err "ccut() must lie strictly between 0 and 0.5"
        exit 198
    }

    mata: st_local("N",  strofreal(gvar_getN()))
    mata: st_local("cn", invtokens(gvar_getcname()'))
    mata: st_local("tv", strofreal(gvar_getT() - gvar_getpmax()))

    tempname R CV
    mata: st_matrix("`R'", gvar_stabtests(`ccut'))
    local nr = rowsof(`R')

    * ---- bootstrap critical values (bootstrap_GVAR_ss.m) -------------------
    * The Toolbox obtains 90, 95 and 99 per cent critical values by
    * regenerating the GVAR, re-estimating every country model and
    * recomputing the whole battery on each replication.
    _gvar_shrinkopt 0 0 "`vcov'" "`shrink'" "`lambda'"
    local vmeth "`r(vmeth)'"
    local vexcl "`r(vexcl)'"
    local shr   "`r(shr)'"
    local lam   "`r(lam)'"

    local shf 0
    if ("`shuffle'" != "") local shf 1
    local dgs 0
    local dgl .
    if ("`shrinkdraw'" != "") local dgs 1
    if ("`lamdraw'" != "" & "`lamdraw'" != "-1") {
        if (`lamdraw' < 0 | `lamdraw' > 1) {
            di as err "lamdraw() must lie between 0 and 1"
            exit 198
        }
        local dgs 1
        local dgl `lamdraw'
    }
    local havecv 0
    if (`reps' > 0) {
        if (`shf' == 0) {
            mata: st_local("dgok", strofreal(gvar_dgpd(`vmeth', `vexcl', ///
                                                       `dgs', `dgl')))
            if ("`dgok'" != "1") {
                mata: st_local("KK", strofreal(gvar_getK()))
                mata: st_local("rk", strofreal(gvar_szetarank()))
                di as err "the bootstrap cannot draw from this covariance:" ///
                          " it is not positive definite"
                di as text "  Sigma_zeta is `KK' by `KK' with rank `rk'."
                di as text "  Add {bf:shuffle}, or {bf:shrinkdraw}."
                exit 506
            }
        }
        * A 95th percentile from B draws is the 0.95*B-th order statistic.
        * These statistics are strongly right-skewed, so with a small B the
        * upper tail is undersampled and the critical value comes out too
        * LOW -- which shows up as over-rejection, not under-rejection.
        if (`reps' < 200) {
            di as text "  {bf:note}: " as result `reps' as text ///
                       " replications is few for a 95th percentile."
            di as text "  The sup-type statistics are right-skewed, so a" ///
                       " small number of"
            di as text "  draws under-samples the upper tail and biases the" ///
                       " critical value"
            di as text "  DOWN, inflating the rejection rate.  Use" ///
                       " {bf:reps(200)} or more"
            di as text "  before reading the rejection counts as evidence."
        }
        di as text "  bootstrapping " as result `reps' as text ///
                   " replications for the critical values ..."
        mata: gvar_bootwrap(`reps', `shf', 4, J(0,1,0), `ccut', 0, 0, ///
                            `vmeth', `vexcl', `shr', `lam', 0, ///
                            0.90, 0.95, 0.99, `dgs', `dgl')
        local bnok   = r_nok
        local bndisc = r_ndisc
        if (`bnok' == 0) {
            di as err "every bootstrap replication failed or was unstable"
            exit 498
        }
        matrix `CV' = r_boot
        local havecv 1
    }

    if ("`tests'" == "") local tests "pk ny qlr"
    local tests = lower("`tests'")

    * counters for rejections against the bootstrap critical values; declared
    * here so -return- and the footer always have something to read, whether
    * or not the table is printed
    local nrej 0
    local ntst 0

    if ("`summary'" != "nosummary") {
        _gvar_title "Structural stability tests of the VECMX* equations"
        di as text "  Sequential Chow trimming: " as result %4.2f `ccut' ///
                   as text " at each end of " as result `tv' as text " observations."
        di ""
        di as text "{hline 100}"
        di as text %-11s "  Unit" %-8s "eq" _col(23) "PKsup" _col(33) "PKmsq" ///
                   _col(43) "Nyblom" _col(54) "robNy" _col(65) "QLR" ///
                   _col(75) "MW" _col(85) "APW" _col(94) "break"
        di as text "{hline 100}"

        forvalues q = 1/`nr' {
            local i = `R'[`q', 1]
            local j = `R'[`q', 2]
            local u : word `i' of `cn'
            mata: st_local("eqn", gvar_getyname(`i', `j'))
            if (`j' == 1) di as text "  " %-9s abbrev("`u'", 9) _continue
            else          di as text "  " %-9s "" _continue
            di as text %-8s abbrev("`eqn'", 8) _continue
            forvalues cc = 3/9 {
                local st " "
                if (`havecv') {
                    local cvv = `CV'[`=`nr'+`q'', `=`cc'-2']
                    if (`R'[`q',`cc'] > `cvv' & `cvv' < .) {
                        local st "*"
                        local ++nrej
                    }
                    local ++ntst
                }
                local cpos = 9 + 10 * (`cc' - 2)
                local val = `R'[`q', `cc']
                di _col(`cpos') as result %8.3f `val' ///
                   as text "`st'" _continue
            }
            di _col(90) as result %6.0f `=`R'[`q',13]'
        }
        di as text "{hline 100}"
        di as text "  PKsup / PKmsq  Ploberger-Kramer OLS-CUSUM and mean-square"
        di as text "  Nyblom / robNy Nyblom LM and its heteroskedasticity-robust form"
        di as text "  QLR / MW / APW Quandt sup-F, mean-F, Andrews-Ploberger exp-F"
        di as text "  break          observation at which the sup-F is attained"
        di ""
        di as text "  These statistics have non-standard distributions, so"
        di as text "  they cannot be read against a chi-squared or F table."
        if (`havecv') {
            di as text "  * marks rejection at 5% against the BOOTSTRAP critical"
            di as text "  value, from " as result `bnok' as text ///
                       " replications" _continue
            if (`bndisc' > 0) {
                di as text " (" as result `bndisc' as text " discarded)."
            }
            else {
                di as text "."
            }
            di as text "  Rejections: " as result `nrej' as text " of " ///
               as result `ntst' as text " (" as result %4.1f ///
               `=100*`nrej'/max(`ntst',1)' as text "%).  Under the null a"
            di as text "  correctly sized battery rejects about 5%."
            di as text "  Use {bf:gvar stability, reps(#) savecv(name)} to keep"
            di as text "  the 90, 95 and 99 per cent values."
        }
        else {
            di as text "  No critical values were computed.  Add" ///
                       " {bf:reps(#) shuffle} to"
            di as text "  bootstrap them as the Toolbox does" ///
                       " (bootstrap_GVAR_ss.m), or read"
            di as text "  them off Table A1 of Dees, di Mauro, Pesaran &" ///
                       " Smith (2007)."
        }
        di ""
    }

    if ("`detail'" != "") {
        di as text "  Robust sequential Chow statistics"
        di as text "{hline 60}"
        di as text %-11s "  Unit" %-8s "eq" _col(24) "robQLR" _col(38) "robMW" ///
                   _col(50) "robAPW"
        di as text "{hline 60}"
        forvalues q = 1/`nr' {
            local i = `R'[`q', 1]
            local j = `R'[`q', 2]
            local u : word `i' of `cn'
            mata: st_local("eqn", gvar_getyname(`i', `j'))
            di as text "  " %-9s abbrev("`u'", 9) %-8s abbrev("`eqn'", 8) ///
               _col(20) as result %11.2f `=`R'[`q',10]' ///
               _col(33) as result %11.2f `=`R'[`q',11]' ///
               _col(46) as result %11.2f `=`R'[`q',12]'
        }
        di as text "{hline 60}"
    }

    * ---- recursive paths ----------------------------------------------------
    * The table above reports maxima and means.  This draws what they are the
    * maximum or the mean OF, which is the only way to see WHERE a rejection
    * comes from and whether it is one date or a drift over the whole sample.
    if ("`graph'" != "") {
        local sel ""
        if ("`equations'" != "") {
            _gvar_xsel "`equations'"
            local xpos "`r(pos)'"
            local xlab "`r(labels)'"
            mata: st_local("xu", invtokens(strofreal(gvar_getxunit())'))
            mata: st_local("xe", invtokens(strofreal(gvar_getxeq())'))
            local w 0
            foreach p of local xpos {
                local ++w
                local uu : word `p' of `xu'
                local ee : word `p' of `xe'
                local ll : word `w' of `xlab'
                local sel "`sel' `uu'|`ee'|`ll'"
            }
        }
        else {
            * no equation named: show the ones the battery likes least, ranked
            * by PKsup relative to its bootstrap critical value where we have
            * one and by PKsup itself where we do not
            tempname RK
            matrix `RK' = J(`nr', 2, .)
            forvalues q = 1/`nr' {
                local sc = `R'[`q', 3]
                if (`havecv') {
                    local cvv = `CV'[`=`nr'+`q'', 1]
                    if (`cvv' < . & `cvv' > 0) local sc = `R'[`q',3] / `cvv'
                }
                matrix `RK'[`q', 1] = `q'
                matrix `RK'[`q', 2] = `sc'
            }
            mata: st_matrix("`RK'", sort(st_matrix("`RK'"), -2))
            local nshow = min(`top', `nr')
            forvalues w = 1/`nshow' {
                local q  = `RK'[`w', 1]
                local uu = `R'[`q', 1]
                local ee = `R'[`q', 2]
                local u : word `uu' of `cn'
                mata: st_local("eqn", gvar_getyname(`uu', `ee'))
                local sel "`sel' `uu'|`ee'|`u':`eqn'"
            }
        }

        local pkcv .
        local qlcv .
        if (`havecv') {
            * the battery's critical values are common to every equation: the
            * bootstrap distribution is taken over replications, so row nr+q
            * of CV is equation q's.  For the reference line we use the median
            * across equations, and say so in the note.
            tempname CVM
            matrix `CVM' = `CV'
            local spk 0
            local sql 0
            local ncv 0
            forvalues q = 1/`nr' {
                * CV columns follow gvar_ssrow: 1 PKsup .. 5 QLR
                local a = `CVM'[`=`nr'+`q'', 1]
                local b = `CVM'[`=`nr'+`q'', 5]
                if (`a' < . & `b' < .) {
                    local spk = `spk' + `a'
                    local sql = `sql' + `b'
                    local ++ncv
                }
            }
            if (`ncv' > 0) {
                local pkcv = `spk' / `ncv'
                local qlcv = `sql' / `ncv'
            }
        }

        _gvar_stab_graph "`sel'" `ccut' `pkcv' `qlcv' "`name'" `havecv'
    }

    if ("`saving'" != "") {
        matrix `saving' = `R'
    }
    if ("`savecv'" != "" & `havecv') {
        matrix `savecv' = `CV'
    }
    if (`havecv') {
        return matrix cv = `CV', copy
        return scalar reps = `bnok'
        return scalar discarded = `bndisc'
    }
    return matrix stability = `R', copy
    return scalar ccut = `ccut'
    return scalar nrej = `nrej'
    return scalar ntest = `ntst'
end

* ---------------------------------------------------------------------------
* Two small-multiple panels in the light palette:
*   left   the Ploberger-Kramer OLS-CUSUM path, with the +/- critical band
*   right  the sequential Chow F path, with the critical line and the break
*
* The CUSUM path is what PKsup maximises; the Chow path is what QLR maximises.
* Reading them beside the table is the difference between knowing that an
* equation is unstable and knowing when it became unstable.
* ---------------------------------------------------------------------------
program define _gvar_stab_graph
    version 14.0
    args sel ccut pkcv qlcv name havecv

    * every colour must be copied out of r() BEFORE the loop: -summarize- and
    * -svmat- inside it overwrite r(), and a glcolor() read late comes back
    * empty
    _gvar_palette
    local c1     "`r(c1)'"
    local c2     "`r(c2)'"
    local band   "`r(band)'"
    local zero   "`r(zero)'"
    local grid   "`r(grid)'"
    local region "`r(region)'"
    local bando  "`r(band_o)'"

    * -graph combine- takes region_options but NOT bgcolor(), which the shared
    * palette region does include, so it needs its own shorter form
    local creg "graphregion(color(white)) plotregion(color(white) lcolor(none))"

    preserve
    quietly {
        local plots ""
        local w 0
        foreach s of local sel {
            local ++w
            local u   = substr("`s'", 1, strpos("`s'", "|") - 1)
            local rst = substr("`s'", strpos("`s'", "|") + 1, .)
            local e   = substr("`rst'", 1, strpos("`rst'", "|") - 1)
            local lab = substr("`rst'", strpos("`rst'", "|") + 1, .)

            clear
            tempname P
            mata: st_matrix("`P'", gvar_stabpath(`u', `e', `ccut'))
            if (rowsof(`P') < 2) continue
            svmat double `P', names(col)
            rename c1 t
            rename c2 cusum
            rename c3 chow
            rename c4 rchow

            * the break date is the argmax of the non-robust Chow path.  The
            * path is missing outside the trimmed interior, so guard on r(N)
            * rather than assuming r(max) exists.
            local bobs = .
            summarize chow, meanonly
            if (r(N) > 0) {
                local cmax = r(max)
                summarize t if abs(chow - `cmax') < 1e-12, meanonly
                if (r(N) > 0) local bobs = r(min)
            }

            * every colour here is an "R G B" triple, so it MUST be quoted:
            * unquoted, Stata reads the first number as a named colour style
            local xt ""
            if (`bobs' < .) {
                local xt `"xline(`bobs', lcolor("`c2'") lpattern(dash) lwidth(medthin))"'
            }

            * ---- CUSUM panel ----
            local cb ""
            if (`pkcv' < .) {
                gen double _hi =  `pkcv'
                gen double _lo = -`pkcv'
                local cb `"(rarea _hi _lo t, color("`band'%`bando'") lwidth(none))"'
            }
            * plain names, not tempnames: -graph combine- needs them to still
            * exist after this program's tempnames would be released, and we
            * drop them ourselves once combined
            local g1 "_gvst_c`w'"
            twoway `cb'                                                     ///
                   (line cusum t, lcolor("`c1'") lwidth(medthick))          ///
                   , yline(0, lcolor("`zero'") lwidth(thin))                ///
                     `xt'                                                   ///
                     legend(off) `region'                                   ///
                     title("`lab'", size(small) color(black))               ///
                     subtitle("OLS-CUSUM", size(vsmall) color(black))       ///
                     xtitle("") ytitle("")                                  ///
                     ylabel(, labsize(vsmall) angle(0) grid glcolor("`grid'")) ///
                     xlabel(, labsize(vsmall))                              ///
                     name(`g1', replace) nodraw
            local plots "`plots' `g1'"
            capture drop _hi _lo

            * ---- sequential Chow panel ----
            local ql ""
            if (`qlcv' < .) {
                local ql `"yline(`qlcv', lcolor("`c2'") lpattern(shortdash) lwidth(medthin))"'
            }
            local g2 "_gvst_f`w'"
            twoway (line chow t, lcolor("`c1'") lwidth(medthick))           ///
                   , `ql' `xt'                                              ///
                     legend(off) `region'                                   ///
                     title(" ", size(small))                                ///
                     subtitle("sequential Chow F", size(vsmall) color(black)) ///
                     xtitle("") ytitle("")                                  ///
                     ylabel(, labsize(vsmall) angle(0) grid glcolor("`grid'")) ///
                     xlabel(, labsize(vsmall))                              ///
                     name(`g2', replace) nodraw
            local plots "`plots' `g2'"
        }
    }
    restore

    local np : word count `plots'
    if (`np' == 0) {
        di as err "no equation produced a usable path"
        exit 498
    }

    local nm "gvar_stability"
    if ("`name'" != "") local nm "`name'"

    * a multi-line note() is a sequence of quoted strings, so the local has to
    * be built with compound quotes or only the first line survives
    local nt `""horizontal axis: observation number within the estimation sample""'
    if (`havecv' == 1) {
        local nt `"`nt' "band and dashed line: bootstrap 5% critical values, averaged over equations""'
        local nt `"`nt' "vertical dash: the break date, the argmax of the sequential Chow F""'
    }
    else {
        local nt `"`nt' "no critical values were computed: add reps(#) shuffle""'
    }

    graph combine `plots', cols(2) `creg'                                   ///
        title("Structural stability of the VECMX* equations",               ///
              size(medsmall) color(black))                                  ///
        note(`nt', size(vsmall) color(black))                               ///
        name(`nm', replace)

    foreach g of local plots {
        capture graph drop `g'
    }

    di as text "  graph saved as {bf:`nm'}"
end

* ---------------------------------------------------------------------------
* The strucchange family of empirical fluctuation processes.
*
* Step -> source map
*   every process, the sdev() scaling, root.matrix() and the drop/rescale of
*   the max functional                       <- strucchange R/efp.R
*   recursive residuals                      <- strucchange R/recresid.R
*   closed-form p-values for Brownian motion
*   and Brownian bridge                      <- strucchange R/efp.R pvalue.efp
*   the two increments tables                <- Chu, Hornik & Kuan (1995)
*                                               Table 1, written inline in
*                                               efp.R, and sc.me from
*                                               R/critvals.R
*
* GVARX's .gvar.stability and vars' stability.varest both call efp() and
* nothing else, so this is the whole of what they offer, applied to each
* VECMX* equation instead of to each VAR equation.
*
* Bootstrap critical values are optional here, unlike for the Toolbox battery:
* efp carries exact asymptotic p-values.  They are still worth having, because
* the asymptotics assume a single equation estimated on independent data and a
* GVAR equation is neither.
* ---------------------------------------------------------------------------
program define _gvar_stab_efp, rclass
    version 14.0
    args efp hfrac reps shf vmeth vexcl shr lam dgs dgl summary graph ///
         equations top name all

    mata: st_local("N",  strofreal(gvar_getN()))
    mata: st_local("cn", invtokens(gvar_getcname()'))

    * ---- which processes ---------------------------------------------------
    local known "rec-cusum ols-cusum rec-mosum ols-mosum re me score-cusum score-mosum"
    local want ""
    if ("`all'" != "") {
        local want "1 2 3 4 5 6 7 8"
    }
    else {
        foreach t of local efp {
            local t = lower("`t'")
            * accept the strucchange spellings and a few obvious short forms
            if ("`t'" == "reccusum")  local t "rec-cusum"
            if ("`t'" == "olscusum")  local t "ols-cusum"
            if ("`t'" == "recmosum")  local t "rec-mosum"
            if ("`t'" == "olsmosum")  local t "ols-mosum"
            if ("`t'" == "scorecusum") local t "score-cusum"
            if ("`t'" == "scoremosum") local t "score-mosum"
            if ("`t'" == "cusum")     local t "ols-cusum"
            if ("`t'" == "mosum")     local t "ols-mosum"
            if ("`t'" == "fluctuation") local t "re"
            local w : list posof "`t'" in known
            if (`w' == 0) {
                di as err "efp(): {bf:`t'} is not one of the processes"
                di as err "Choose from, in strucchange's own names:"
                di as err "    {bf:Rec-CUSUM  OLS-CUSUM  Rec-MOSUM  OLS-MOSUM}"
                di as err "    {bf:RE  ME  Score-CUSUM  Score-MOSUM}"
                di as err "{bf:fluctuation} is accepted as a synonym for RE," ///
                          " as in efp()."
                exit 198
            }
            local already : list posof "`w'" in want
            if (`already' == 0) local want "`want' `w'"
        }
    }
    local nw : word count `want'

    local lname1 "Rec-CUSUM"
    local lname2 "OLS-CUSUM"
    local lname3 "Rec-MOSUM"
    local lname4 "OLS-MOSUM"
    local lname5 "RE"
    local lname6 "ME"
    local lname7 "Score-CUSUM"
    local lname8 "Score-MOSUM"
    local llim1 "Brownian motion"
    local llim2 "Brownian bridge"
    local llim3 "BM increments"
    local llim4 "BB increments"
    local llim5 "Brownian bridge"
    local llim6 "BB increments"
    local llim7 "Brownian bridge"
    local llim8 "BB increments"

    tempname S
    local nrej 0
    local ntst 0

    if ("`summary'" != "nosummary") {
        _gvar_title "Empirical fluctuation processes of the VECMX* equations"
        di as text "  Window fraction h = " as result %4.2f `hfrac' ///
           as text ".  Statistic: the max functional."
        di as text "  Source: {bf:strucchange::efp}, which is what GVARX and" ///
                   " vars call."
        di ""
    }

    foreach w of local want {
        mata: st_matrix("`S'", gvar_efptests(`w', `hfrac'))
        local nr = rowsof(`S')

        * bootstrap critical values for this process, if asked for
        local havecv 0
        if (`reps' > 0) {
            di as text "  bootstrapping " as result `reps' ///
               as text " replications for {bf:`lname`w''} ..."
            mata: gvar_bootwrap(`reps', `shf', 5, J(0,1,0), `hfrac', 0, `w', ///
                                `vmeth', `vexcl', `shr', `lam', 0, ///
                                0.90, 0.95, 0.99, `dgs', `dgl')
            local bnok = r_nok
            if (`bnok' > 0) {
                tempname CV
                matrix `CV' = r_boot
                local havecv 1
            }
        }

        * Count the rejections BEFORE the display block.  Counting inside it
        * means nosummary returns 0 of 0, which has bitten this package four
        * times: any counter that -return- reports has to be accumulated
        * outside every display guard.
        forvalues q = 1/`nr' {
            local pv = `S'[`q', 4]
            if (`pv' < .) {
                local ++ntst
                if (`pv' < 0.05) local ++nrej
            }
        }

        if ("`summary'" != "nosummary") {
            local wid 74
            if (`havecv') local wid 88
            di as text "{hline `wid'}"
            di as text "  {bf:`lname`w''}" _col(30) "limit: `llim`w''"
            di as text "{hline `wid'}"
            di as text %-11s "  Unit" %-9s "eq" _col(24) "statistic" ///
               _col(38) "p-value" _col(50) "comp" _continue
            if (`havecv') {
                di as text _col(60) "5% boot" _col(74) "1% boot"
            }
            else {
                di ""
            }
            di as text "{hline `wid'}"

            forvalues q = 1/`nr' {
                local i = `S'[`q', 1]
                local j = `S'[`q', 2]
                local u : word `i' of `cn'
                mata: st_local("eqn", gvar_getyname(`i', `j'))
                if (`j' == 1) di as text "  " %-9s abbrev("`u'", 9) _continue
                else          di as text "  " %-9s "" _continue
                di as text %-9s abbrev("`eqn'", 9) _continue

                local pv = `S'[`q', 4]
                local st ""
                if (`pv' < .) {
                    _gvar_stars `pv'
                    local st "`r(stars)'"
                }
                di _col(22) as result %10.4f `=`S'[`q',3]' ///
                   _col(35) as result %9.4f `pv' as text "`st'" ///
                   _col(49) as result %5.0f `=`S'[`q',5]' _continue
                if (`havecv') {
                    di _col(57) as result %10.4f `=`CV'[`=`nr'+`q'', 1]' ///
                       _col(71) as result %10.4f `=`CV'[`=2*`nr'+`q'', 1]'
                }
                else {
                    di ""
                }
            }
            di as text "{hline `wid'}"
            di ""
        }
    }

    if ("`summary'" != "nosummary") {
        di as text "  {bf:comp} is the number of components of the process," ///
                   " strucchange's k:"
        di as text "  one for the CUSUM and MOSUM processes, one per" ///
                   " coefficient for RE and"
        di as text "  ME, and one per coefficient plus one for the variance" ///
                   " score in the"
        di as text "  Score processes.  The p-value depends on it, and" ///
                   " pvalue.efp caps it"
        di as text "  at 6 for the Brownian-bridge-increments table -- so" ///
                   " for ME and"
        di as text "  Score-MOSUM on a GVAR equation the p-value is the" ///
                   " k = 6 one and is"
        di as text "  {bf:conservative}, not exact."
        di ""
        if (`ntst' > 0) {
            di as text "  Rejections at 5%: " as result `nrej' as text " of " ///
               as result `ntst' as text " (" as result %4.1f ///
               `=100*`nrej'/`ntst'' as text "%)."
        }
        di as text "  The asymptotics behind these p-values assume one" ///
                   " equation estimated"
        di as text "  on its own data.  A GVAR equation shares its foreign" ///
                   " variables with"
        di as text "  every other, so add {bf:reps(#) shuffle} for critical" ///
                   " values that"
        di as text "  come from the model itself."
        di ""
        di as text "  * 10%  ** 5%  *** 1%"
        di ""
    }

    if ("`graph'" != "") {
        local w1 : word 1 of `want'
        _gvar_efp_graph `w1' `hfrac' "`equations'" `top' "`name'" ///
                        "`lname`w1''"
    }

    return matrix efp = `S', copy
    return scalar nrej  = `nrej'
    return scalar ntest = `ntst'
    return scalar hfrac = `hfrac'
    return local  types "`want'"
end

* ---------------------------------------------------------------------------
* The fluctuation path of one process for a few equations, with its critical
* value drawn as a horizontal line.  For Brownian motion the process has
* already been divided by its (1 + 2t) boundary, so a flat line is right for
* every type.
* ---------------------------------------------------------------------------
program define _gvar_efp_graph
    version 14.0
    args which hfrac equations top name lname

    _gvar_palette
    local c1   "`r(c1)'"
    local c2   "`r(c2)'"
    local grid "`r(grid)'"
    local reg  "`r(region)'"
    local creg "graphregion(color(white)) plotregion(color(white) lcolor(none))"

    local nm "gvar_efp"
    if ("`name'" != "") local nm "`name'"

    mata: st_local("cn", invtokens(gvar_getcname()'))

    * pick the equations
    tempname S
    mata: st_matrix("`S'", gvar_efptests(`which', `hfrac'))
    local nr = rowsof(`S')

    local sel ""
    if ("`equations'" != "") {
        _gvar_xsel "`equations'"
        local xpos "`r(pos)'"
        local xlab "`r(labels)'"
        mata: st_local("xu", invtokens(strofreal(gvar_getxunit())'))
        mata: st_local("xe", invtokens(strofreal(gvar_getxeq())'))
        local w 0
        foreach p of local xpos {
            local ++w
            local uu : word `p' of `xu'
            local ee : word `p' of `xe'
            local ll : word `w' of `xlab'
            local sel "`sel' `uu'|`ee'|`ll'"
        }
    }
    else {
        * the equations with the smallest p-values
        tempname RK
        matrix `RK' = J(`nr', 2, .)
        forvalues q = 1/`nr' {
            matrix `RK'[`q', 1] = `q'
            matrix `RK'[`q', 2] = `S'[`q', 4]
        }
        mata: st_matrix("`RK'", sort(st_matrix("`RK'"), 2))
        local nshow = min(`top', `nr')
        forvalues w = 1/`nshow' {
            local q  = `RK'[`w', 1]
            local uu = `S'[`q', 1]
            local ee = `S'[`q', 2]
            local u : word `uu' of `cn'
            mata: st_local("eqn", gvar_getyname(`uu', `ee'))
            local sel "`sel' `uu'|`ee'|`u':`eqn'"
        }
    }

    preserve
    local plots ""
    local w 0
    quietly {
        foreach s of local sel {
            local ++w
            local u   = substr("`s'", 1, strpos("`s'", "|") - 1)
            local rst = substr("`s'", strpos("`s'", "|") + 1, .)
            local e   = substr("`rst'", 1, strpos("`rst'", "|") - 1)
            local lab = substr("`rst'", strpos("`rst'", "|") + 1, .)

            clear
            tempname P
            mata: st_matrix("`P'", gvar_efppath(`u', `e', `which', `hfrac'))
            if (rowsof(`P') < 2) continue
            svmat double `P', names(col)
            rename c1 t
            rename c2 v

            * the 5% critical value for this equation's component count, found
            * by inverting the p-value function
            local q5 .
            forvalues qq = 1/`nr' {
                if (`S'[`qq',1] == `u' & `S'[`qq',2] == `e') {
                    local nc = `S'[`qq', 5]
                    mata: st_local("q5", strofreal(gvar_efpcrit(`which', ///
                                   `hfrac', `nc', 0.05)))
                }
            }
            local cl ""
            if ("`q5'" != "" & "`q5'" != ".") {
                local cl `"yline(`q5', lcolor("`c2'") lpattern(dash) lwidth(medthin))"'
            }

            local gn "_gvefp`w'"
            twoway (line v t, lcolor("`c1'") lwidth(medthick))              ///
                   , `reg' `cl'                                            ///
                     ylabel(, angle(0) labsize(vsmall) grid                 ///
                            glcolor("`grid'"))                              ///
                     xlabel(, labsize(vsmall))                              ///
                     ytitle("") xtitle("")                                  ///
                     title("`lab'", size(small) color(black))               ///
                     legend(off) name(`gn', replace) nodraw
            local plots "`plots' `gn'"
        }
    }
    restore

    local np : word count `plots'
    if (`np' == 0) {
        di as err "no equation produced a usable process"
        exit 498
    }
    local cols 2
    if (`np' == 1) local cols 1
    if (`np' > 6)  local cols 3

    graph combine `plots', cols(`cols') `creg'                              ///
        title("`lname' fluctuation process", size(medsmall) color(black))   ///
        note("dashed: the 5% critical value"                                 ///
             "the path is the max-norm across components, after the drop"    ///
             " and rescale sctest applies",                                  ///
             size(vsmall) color(black))                                     ///
        name(`nm', replace)

    foreach g of local plots {
        capture graph drop `g'
    }
    di as text "  graph saved as {bf:`nm'}"
end