antecedent.discovery

Discovery stubs.

  1"""Discovery algorithm configuration and helpers."""
  2
  3from __future__ import annotations
  4
  5from dataclasses import dataclass
  6from typing import Any, Callable, Literal, Sequence, Union
  7
  8import numpy as np
  9from numpy.typing import NDArray
 10
 11from ._native import (
 12    DiscoveredLink,
 13    GraphEdge,
 14    GraphPosterior,
 15    PcmciDiscoveryResult,
 16    RpcmciDiscoverySummary,
 17    discover_ci_screened_posterior as _discover_ci_screened_posterior,
 18    discover_dbn_posterior as _discover_dbn_posterior,
 19    discover_exact_dag_posterior as _discover_exact_dag_posterior,
 20    discover_jpcmci_plus as _discover_jpcmci_plus,
 21    discover_lpcmci as _discover_lpcmci,
 22    discover_order_mcmc as _discover_order_mcmc,
 23    discover_pc as _discover_pc,
 24    discover_ges as _discover_ges,
 25    discover_lingam as _discover_lingam,
 26    discover_notears as _discover_notears,
 27    discover_fci as _discover_fci,
 28    discover_pcmci as _discover_pcmci,
 29    discover_pcmci_plus as _discover_pcmci_plus,
 30    discover_rfci as _discover_rfci,
 31    discover_rpcmci as _discover_rpcmci,
 32    discover_structure_mcmc as _discover_structure_mcmc,
 33    two_regime_half_split,
 34)
 35
 36CiSpec = Union[str, Callable[..., Sequence[tuple[float, float]]]]
 37
 38
 39@dataclass(frozen=True)
 40class PC:
 41    alpha: float = 0.05
 42    fdr: bool = True
 43    ci: CiSpec = "parcorr"
 44    max_cond_size: int = 2
 45    kind: Literal["pc"] = "pc"
 46
 47
 48@dataclass(frozen=True)
 49class PCMCI:
 50    max_lag: int = 1
 51    alpha: float = 0.05
 52    fdr: bool = True
 53    ci: CiSpec = "parcorr"
 54    kind: Literal["pcmci"] = "pcmci"
 55
 56
 57@dataclass(frozen=True)
 58class PCMCIPlus:
 59    max_lag: int = 1
 60    alpha: float = 0.05
 61    fdr: bool = True
 62    ci: CiSpec = "parcorr"
 63    kind: Literal["pcmci_plus"] = "pcmci_plus"
 64
 65
 66@dataclass(frozen=True)
 67class LPCMCI:
 68    max_lag: int = 1
 69    alpha: float = 0.05
 70    fdr: bool = True
 71    ci: CiSpec = "parcorr"
 72    kind: Literal["lpcmci"] = "lpcmci"
 73
 74
 75@dataclass(frozen=True)
 76class JPCMCIPlus:
 77    max_lag: int = 1
 78    alpha: float = 0.05
 79    fdr: bool = True
 80    ci: CiSpec = "parcorr"
 81    context_names: tuple[str, ...] = ()
 82    include_space_dummy: bool = True
 83    include_time_dummy: bool = False
 84    space_dummy_ci: Literal["scalar", "multivariate"] = "scalar"
 85    time_dummy_encoding: Literal["integer", "one_hot"] = "integer"
 86    time_dummy_ci: Literal["scalar", "multivariate"] = "scalar"
 87    kind: Literal["jpcmci_plus"] = "jpcmci_plus"
 88
 89
 90@dataclass(frozen=True)
 91class RPCMCI:
 92    """Regime-PCMCI. Pass ``regimes=`` to ``analyze`` / ``discover_rpcmci`` (required).
 93
 94    Use ``two_regime_half_split(n)`` when a simple half-split label vector is enough.
 95    """
 96
 97    max_lag: int = 1
 98    alpha: float = 0.05
 99    fdr: bool = True
100    ci: CiSpec = "parcorr"
101    kind: Literal["rpcmci"] = "rpcmci"
102
103
104@dataclass(frozen=True)
105class GES:
106    alpha: float = 0.05
107    fdr: bool = True
108    ci: CiSpec = "parcorr"
109    max_cond_size: int = 2
110    screen_pc: bool = False
111    max_subset: int | None = None
112    kind: Literal["ges"] = "ges"
113
114
115@dataclass(frozen=True)
116class LiNGAM:
117    prune_threshold: float = 0.05
118    max_cond_size: int = 8
119    kind: Literal["lingam"] = "lingam"
120
121
122@dataclass(frozen=True)
123class NOTEARS:
124    l1: float = 0.1
125    threshold: float = 0.3
126    standardize: bool = True
127    max_cond_size: int = 8
128    kind: Literal["notears"] = "notears"
129
130
131@dataclass(frozen=True)
132class FCI:
133    alpha: float = 0.05
134    fdr: bool = True
135    ci: CiSpec = "parcorr"
136    max_cond_size: int = 2
137    kind: Literal["fci"] = "fci"
138
139
140@dataclass(frozen=True)
141class RFCI:
142    alpha: float = 0.05
143    fdr: bool = True
144    ci: CiSpec = "parcorr"
145    max_cond_size: int = 2
146    kind: Literal["rfci"] = "rfci"
147
148
149@dataclass(frozen=True)
150class ExactDagPosterior:
151    """Exact DAG posterior enumeration (hard limit: n ≤ 6, Gaussian BIC).
152
153    For more variables use ``OrderMcmc``, ``StructureMcmc``, or ``CiScreenedPosterior``.
154
155    Standalone: ``discover_exact_dag_posterior(...)`` returns a ``GraphPosterior``.
156    Composed: pass ``discovery=ExactDagPosterior()`` with ``inference=Bayesian(...)``
157    to ``analyze`` to mix effect draws over the graph posterior (P1-D).
158    """
159
160    kind: Literal["exact_dag_posterior"] = "exact_dag_posterior"
161
162
163@dataclass(frozen=True)
164class OrderMcmc:
165    n_chains: int = 4
166    n_warmup: int = 500
167    n_draws: int = 1000
168    thin: int = 1
169    require_diagnostics_gate: bool = True
170    kind: Literal["order_mcmc"] = "order_mcmc"
171
172
173@dataclass(frozen=True)
174class StructureMcmc:
175    n_chains: int = 4
176    n_warmup: int = 500
177    n_draws: int = 1000
178    thin: int = 1
179    kind: Literal["structure_mcmc"] = "structure_mcmc"
180
181
182@dataclass(frozen=True)
183class CiScreenedPosterior:
184    alpha: float = 0.05
185    fdr: bool = True
186    ci: str = "parcorr"
187    max_cond_size: int = 2
188    soft_weight: Literal["none", "bayes_factor", "posterior_dependence"] = "none"
189    n_chains: int = 2
190    n_warmup: int = 300
191    n_draws: int = 600
192    thin: int = 1
193    kind: Literal["ci_screened_posterior"] = "ci_screened_posterior"
194
195
196@dataclass(frozen=True)
197class DbnPosterior:
198    """Bounded-lag DBN posterior (Gaussian BIC).
199
200    Exact enumeration only when ``p ≤ 4`` and ``max_lag ≤ 2``; larger templates
201    automatically use MCMC (or set ``force_mcmc=True``).
202
203    Standalone: ``discover_dbn_posterior(...)`` returns a ``GraphPosterior``.
204    Composed: pass ``discovery=DbnPosterior(...)`` with ``inference=Bayesian(...)``
205    and ``PulseEffect``/``SustainedEffect`` to mix temporal effect draws (P1-D).
206    """
207
208    max_lag: int = 1
209    force_mcmc: bool = False
210    n_chains: int = 2
211    n_warmup: int = 200
212    n_draws: int = 400
213    kind: Literal["dbn_posterior"] = "dbn_posterior"
214
215
216# Alias: DiscoveryResult is the preferred name; PcmciDiscoveryResult kept for compat.
217DiscoveryResult = PcmciDiscoveryResult
218
219
220def discovery_to_dag(result: DiscoveryResult) -> "Dag":
221    """Build a ``Dag`` from a discovery result's directed ``graph_edges``.
222
223    Raises ``ValueError`` if any undirected/circle marks remain.
224    """
225    from .graph import Dag
226
227    names: list[str] = []
228    seen: set[str] = set()
229    directed: list[tuple[str, str]] = []
230    for e in result.graph_edges:
231        for n in (e.source, e.target):
232            if n not in seen:
233                seen.add(n)
234                names.append(n)
235        if e.at_source == "tail" and e.at_target == "arrow":
236            directed.append((e.source, e.target))
237        elif e.at_source == "arrow" and e.at_target == "tail":
238            directed.append((e.target, e.source))
239        else:
240            raise ValueError(
241                f"cannot coerce edge {e.source}->{e.target} "
242                f"({e.at_source}/{e.at_target}) into a DAG; "
243                "use graph_edges or a CPDAG/PAG constructor"
244            )
245    return Dag.from_edges(names, directed)
246
247
248def _coerce_tabular(
249    names_or_data: Any | None = None,
250    columns: Sequence[NDArray[np.float64]] | None = None,
251    *,
252    data: Any | None = None,
253    names: list[str] | None = None,
254) -> tuple[list[str], list[NDArray[np.float64]]]:
255    """Accept ``discover_*(data)``, ``discover_*(names, columns)``, or kwargs."""
256    from ._data import as_columns, coerce_data_args, to_f64
257
258    if data is not None:
259        return as_columns(data)
260    if columns is not None:
261        if names_or_data is None:
262            raise TypeError("columns= requires names as the first argument")
263        return [str(n) for n in names_or_data], [to_f64(c) for c in columns]
264    if names is not None:
265        # names= kw-only without columns — need columns via coerce
266        return coerce_data_args(None, names=names, columns=None)
267    if names_or_data is not None:
268        return as_columns(names_or_data)
269    raise TypeError("provide data=… or names + columns")
270
271
272def discover_pc(
273    names: Any | None = None,
274    columns: Sequence[NDArray[np.float64]] | None = None,
275    *,
276    data: Any | None = None,
277    alpha: float = 0.05,
278    fdr: bool = True,
279    seed: int = 1,
280    ci: str = "parcorr",
281    max_cond_size: int = 2,
282    threads: int = 1,
283) -> DiscoveryResult:
284    n, cols = _coerce_tabular(names, columns, data=data)
285    return _discover_pc(
286        n,
287        cols,
288        alpha=alpha,
289        fdr=fdr,
290        seed=seed,
291        ci=ci,
292        max_cond_size=max_cond_size,
293        threads=threads,
294    )
295
296
297def discover_ges(
298    names: Any | None = None,
299    columns: Sequence[NDArray[np.float64]] | None = None,
300    *,
301    data: Any | None = None,
302    alpha: float = 0.05,
303    fdr: bool = True,
304    seed: int = 1,
305    ci: str = "parcorr",
306    max_cond_size: int = 2,
307    threads: int = 1,
308    screen_pc: bool = False,
309    max_subset: int | None = None,
310) -> DiscoveryResult:
311    n, cols = _coerce_tabular(names, columns, data=data)
312    return _discover_ges(
313        n,
314        cols,
315        alpha=alpha,
316        fdr=fdr,
317        seed=seed,
318        ci=ci,
319        max_cond_size=max_cond_size,
320        threads=threads,
321        screen_pc=screen_pc,
322        max_subset=max_subset,
323    )
324
325
326def discover_lingam(
327    names: Any | None = None,
328    columns: Sequence[NDArray[np.float64]] | None = None,
329    *,
330    data: Any | None = None,
331    prune_threshold: float = 0.05,
332    seed: int = 1,
333    max_cond_size: int = 8,
334    threads: int = 1,
335) -> DiscoveryResult:
336    n, cols = _coerce_tabular(names, columns, data=data)
337    return _discover_lingam(
338        n,
339        cols,
340        prune_threshold=prune_threshold,
341        seed=seed,
342        max_cond_size=max_cond_size,
343        threads=threads,
344    )
345
346
347def discover_notears(
348    names: Any | None = None,
349    columns: Sequence[NDArray[np.float64]] | None = None,
350    *,
351    data: Any | None = None,
352    l1: float = 0.1,
353    threshold: float = 0.3,
354    standardize: bool = True,
355    seed: int = 1,
356    max_cond_size: int = 8,
357    threads: int = 1,
358) -> DiscoveryResult:
359    n, cols = _coerce_tabular(names, columns, data=data)
360    return _discover_notears(
361        n,
362        cols,
363        l1=l1,
364        threshold=threshold,
365        standardize=standardize,
366        seed=seed,
367        max_cond_size=max_cond_size,
368        threads=threads,
369    )
370
371
372def discover_fci(
373    names: Any | None = None,
374    columns: Sequence[NDArray[np.float64]] | None = None,
375    *,
376    data: Any | None = None,
377    alpha: float = 0.05,
378    fdr: bool = True,
379    seed: int = 1,
380    ci: str = "parcorr",
381    max_cond_size: int = 2,
382    threads: int = 1,
383) -> DiscoveryResult:
384    n, cols = _coerce_tabular(names, columns, data=data)
385    return _discover_fci(
386        n,
387        cols,
388        alpha=alpha,
389        fdr=fdr,
390        seed=seed,
391        ci=ci,
392        max_cond_size=max_cond_size,
393        threads=threads,
394    )
395
396
397def discover_rfci(
398    names: Any | None = None,
399    columns: Sequence[NDArray[np.float64]] | None = None,
400    *,
401    data: Any | None = None,
402    alpha: float = 0.05,
403    fdr: bool = True,
404    seed: int = 1,
405    ci: str = "parcorr",
406    max_cond_size: int = 2,
407    threads: int = 1,
408) -> DiscoveryResult:
409    n, cols = _coerce_tabular(names, columns, data=data)
410    return _discover_rfci(
411        n,
412        cols,
413        alpha=alpha,
414        fdr=fdr,
415        seed=seed,
416        ci=ci,
417        max_cond_size=max_cond_size,
418        threads=threads,
419    )
420
421
422def discover_pcmci(
423    names: Any | None = None,
424    columns: Sequence[NDArray[np.float64]] | None = None,
425    *,
426    data: Any | None = None,
427    max_lag: int = 1,
428    alpha: float = 0.05,
429    fdr: bool = True,
430    seed: int = 1,
431    ci: str = "parcorr",
432    weights: list[float] | None = None,
433    threads: int = 1,
434) -> DiscoveryResult:
435    n, cols = _coerce_tabular(names, columns, data=data)
436    return _discover_pcmci(
437        n,
438        cols,
439        max_lag=max_lag,
440        alpha=alpha,
441        fdr=fdr,
442        seed=seed,
443        ci=ci,
444        weights=weights,
445        threads=threads,
446    )
447
448
449def discover_pcmci_plus(
450    names: Any | None = None,
451    columns: Sequence[NDArray[np.float64]] | None = None,
452    *,
453    data: Any | None = None,
454    max_lag: int = 1,
455    alpha: float = 0.05,
456    fdr: bool = True,
457    seed: int = 1,
458    ci: str = "parcorr",
459    weights: list[float] | None = None,
460    threads: int = 1,
461) -> DiscoveryResult:
462    n, cols = _coerce_tabular(names, columns, data=data)
463    return _discover_pcmci_plus(
464        n,
465        cols,
466        max_lag=max_lag,
467        alpha=alpha,
468        fdr=fdr,
469        seed=seed,
470        ci=ci,
471        weights=weights,
472        threads=threads,
473    )
474
475
476def discover_lpcmci(
477    names: Any | None = None,
478    columns: Sequence[NDArray[np.float64]] | None = None,
479    *,
480    data: Any | None = None,
481    max_lag: int = 1,
482    alpha: float = 0.05,
483    fdr: bool = True,
484    seed: int = 1,
485    ci: str = "parcorr",
486    weights: list[float] | None = None,
487    threads: int = 1,
488) -> DiscoveryResult:
489    n, cols = _coerce_tabular(names, columns, data=data)
490    return _discover_lpcmci(
491        n,
492        cols,
493        max_lag=max_lag,
494        alpha=alpha,
495        fdr=fdr,
496        seed=seed,
497        ci=ci,
498        weights=weights,
499        threads=threads,
500    )
501
502
503def discover_jpcmci_plus(
504    names: list[str],
505    env_columns: Sequence[Sequence[NDArray[np.float64]]],
506    *,
507    max_lag: int = 1,
508    alpha: float = 0.05,
509    fdr: bool = True,
510    seed: int = 1,
511    ci: str = "parcorr",
512    weights: list[float] | None = None,
513    threads: int = 1,
514    context_names: Sequence[str] | None = None,
515    include_space_dummy: bool = True,
516    include_time_dummy: bool = False,
517    space_dummy_ci: str = "scalar",
518    time_dummy_encoding: str = "integer",
519    time_dummy_ci: str = "scalar",
520) -> PcmciDiscoveryResult:
521    return _discover_jpcmci_plus(
522        names,
523        [list(cols) for cols in env_columns],
524        max_lag=max_lag,
525        alpha=alpha,
526        fdr=fdr,
527        seed=seed,
528        ci=ci,
529        weights=weights,
530        threads=threads,
531        context_names=list(context_names) if context_names is not None else None,
532        include_space_dummy=include_space_dummy,
533        include_time_dummy=include_time_dummy,
534        space_dummy_ci=space_dummy_ci,
535        time_dummy_encoding=time_dummy_encoding,
536        time_dummy_ci=time_dummy_ci,
537    )
538
539
540def discover_rpcmci(
541    names: Any | None = None,
542    columns: Sequence[NDArray[np.float64]] | None = None,
543    *,
544    data: Any | None = None,
545    regimes: Sequence[int],
546    max_lag: int = 1,
547    alpha: float = 0.05,
548    fdr: bool = True,
549    seed: int = 1,
550    ci: str = "parcorr",
551    weights: list[float] | None = None,
552    threads: int = 1,
553) -> RpcmciDiscoverySummary:
554    """Run RPCMCI. ``regimes`` is required (length = series length); no silent half-split.
555
556    Call ``two_regime_half_split(len(series))`` for an explicit two-regime mid-point split.
557    """
558    n, cols = _coerce_tabular(names, columns, data=data)
559    return _discover_rpcmci(
560        n,
561        cols,
562        regimes=list(regimes),
563        max_lag=max_lag,
564        alpha=alpha,
565        fdr=fdr,
566        seed=seed,
567        ci=ci,
568        weights=weights,
569        threads=threads,
570    )
571
572
573def discover_exact_dag_posterior(
574    names: Any | None = None,
575    columns: Sequence[NDArray[np.float64]] | None = None,
576    *,
577    data: Any | None = None,
578    seed: int = 1,
579    threads: int = 1,
580) -> GraphPosterior:
581    """Exact DAG posterior (hard limit n ≤ 6). Prefer MCMC helpers for larger graphs."""
582    n, cols = _coerce_tabular(names, columns, data=data)
583    return _discover_exact_dag_posterior(n, cols, seed=seed, threads=threads)
584
585
586def discover_order_mcmc(
587    names: Any | None = None,
588    columns: Sequence[NDArray[np.float64]] | None = None,
589    *,
590    data: Any | None = None,
591    n_chains: int = 4,
592    n_warmup: int = 500,
593    n_draws: int = 1000,
594    thin: int = 1,
595    require_diagnostics_gate: bool = True,
596    seed: int = 1,
597    threads: int = 1,
598) -> GraphPosterior:
599    n, cols = _coerce_tabular(names, columns, data=data)
600    return _discover_order_mcmc(
601        n,
602        cols,
603        n_chains=n_chains,
604        n_warmup=n_warmup,
605        n_draws=n_draws,
606        thin=thin,
607        require_diagnostics_gate=require_diagnostics_gate,
608        seed=seed,
609        threads=threads,
610    )
611
612
613def discover_structure_mcmc(
614    names: Any | None = None,
615    columns: Sequence[NDArray[np.float64]] | None = None,
616    *,
617    data: Any | None = None,
618    n_chains: int = 4,
619    n_warmup: int = 500,
620    n_draws: int = 1000,
621    thin: int = 1,
622    seed: int = 1,
623    threads: int = 1,
624) -> GraphPosterior:
625    n, cols = _coerce_tabular(names, columns, data=data)
626    return _discover_structure_mcmc(
627        n,
628        cols,
629        n_chains=n_chains,
630        n_warmup=n_warmup,
631        n_draws=n_draws,
632        thin=thin,
633        seed=seed,
634        threads=threads,
635    )
636
637
638def discover_ci_screened_posterior(
639    names: Any | None = None,
640    columns: Sequence[NDArray[np.float64]] | None = None,
641    *,
642    data: Any | None = None,
643    alpha: float = 0.05,
644    fdr: bool = True,
645    seed: int = 1,
646    ci: str = "parcorr",
647    max_cond_size: int = 2,
648    soft_weight: str = "none",
649    n_chains: int = 2,
650    n_warmup: int = 300,
651    n_draws: int = 600,
652    thin: int = 1,
653    threads: int = 1,
654) -> GraphPosterior:
655    n, cols = _coerce_tabular(names, columns, data=data)
656    return _discover_ci_screened_posterior(
657        n,
658        cols,
659        alpha=alpha,
660        fdr=fdr,
661        ci=ci,
662        max_cond_size=max_cond_size,
663        soft_weight=soft_weight,
664        n_chains=n_chains,
665        n_warmup=n_warmup,
666        n_draws=n_draws,
667        thin=thin,
668        seed=seed,
669        threads=threads,
670    )
671
672
673def discover_dbn_posterior(
674    names: Any | None = None,
675    columns: Sequence[NDArray[np.float64]] | None = None,
676    *,
677    data: Any | None = None,
678    max_lag: int = 1,
679    force_mcmc: bool = False,
680    n_chains: int = 2,
681    n_warmup: int = 200,
682    n_draws: int = 400,
683    seed: int = 1,
684    threads: int = 1,
685) -> GraphPosterior:
686    """DBN template posterior; exact only for p ≤ 4 and max_lag ≤ 2, else MCMC."""
687    n, cols = _coerce_tabular(names, columns, data=data)
688    return _discover_dbn_posterior(
689        n,
690        cols,
691        max_lag=max_lag,
692        force_mcmc=force_mcmc,
693        n_chains=n_chains,
694        n_warmup=n_warmup,
695        n_draws=n_draws,
696        seed=seed,
697        threads=threads,
698    )
699
700
701
702def graph_posterior_map_edges(post: "GraphPosterior") -> list[tuple[str, str]]:
703    """Oriented edges from the maximum-weight adjacency mask in a graph posterior."""
704    import numpy as np
705
706    if post.n_graphs < 1 or not post.weights:
707        raise ValueError("GraphPosterior has no graphs")
708    i = int(np.argmax(np.asarray(post.weights, dtype=np.float64)))
709    mask = int(post.adjacency[i])
710    n = int(post.n_vars)
711    names = list(post.names)
712    if len(names) < n:
713        names = [f"x{j}" for j in range(n)]
714    edges: list[tuple[str, str]] = []
715    for fr in range(n):
716        for to in range(n):
717            if fr == to:
718                continue
719            # edge_bit(n, from, to) = from*(n-1) + (to if to < from else to-1)
720            bit = fr * (n - 1) + (to if to < fr else to - 1)
721            if (mask >> bit) & 1:
722                edges.append((names[fr], names[to]))
723    return edges
724
725
726def graph_posterior_map_dag(post: "GraphPosterior") -> "Dag":
727    """MAP DAG from a graph posterior (maximum-weight atom)."""
728    from .graph import Dag
729
730    edges = graph_posterior_map_edges(post)
731    names = list(post.names) if post.names else sorted({a for e in edges for a in e})
732    return Dag.from_edges(names, edges)
733
734
735def cpdag_oriented_edges(cpdag: "Cpdag", *, require_oriented: bool = True) -> list[tuple[str, str]]:
736    """Return directed edges from a CPDAG; error if undirected remain when required."""
737    from .graph import Cpdag
738
739    if not isinstance(cpdag, Cpdag):
740        raise TypeError(f"expected Cpdag, got {type(cpdag)!r}")
741    directed: list[tuple[str, str]] = []
742    undirected = 0
743    for src, tgt, kind in cpdag.edges():
744        if kind == "directed":
745            directed.append((src, tgt))
746        elif kind == "undirected":
747            undirected += 1
748        else:
749            undirected += 1
750    if undirected and require_oriented:
751        raise ValueError(
752            f"CPDAG has {undirected} undirected/ambiguous edge(s); orient before "
753            "PathSpecific/Interventional queries (require_oriented=True)"
754        )
755    if require_oriented:
756        try:
757            dag = cpdag.try_into_dag()
758        except Exception as exc:  # noqa: BLE001
759            raise ValueError(
760                "CPDAG is not fully oriented; cannot coerce to DAG for path/distribution queries"
761            ) from exc
762        return list(dag.edges())
763    return directed
764
765
766__all__ = [
767    "CiScreenedPosterior",
768    "DbnPosterior",
769    "DiscoveredLink",
770    "DiscoveryResult",
771    "ExactDagPosterior",
772    "FCI",
773    "GES",
774    "GraphEdge",
775    "GraphPosterior",
776    "JPCMCIPlus",
777    "LPCMCI",
778    "LiNGAM",
779    "NOTEARS",
780    "OrderMcmc",
781    "PC",
782    "PCMCI",
783    "PCMCIPlus",
784    "PcmciDiscoveryResult",
785    "RFCI",
786    "RPCMCI",
787    "RpcmciDiscoverySummary",
788    "StructureMcmc",
789    "discover_ci_screened_posterior",
790    "discover_dbn_posterior",
791    "discover_exact_dag_posterior",
792    "discover_jpcmci_plus",
793    "discover_lpcmci",
794    "discover_order_mcmc",
795    "discover_pc",
796    "discover_ges",
797    "discover_lingam",
798    "discover_notears",
799    "discover_fci",
800    "discover_rfci",
801    "discover_pcmci",
802    "discover_pcmci_plus",
803    "discover_rpcmci",
804    "discover_structure_mcmc",
805    "cpdag_oriented_edges",
806    "discovery_to_dag",
807    "graph_posterior_map_dag",
808    "graph_posterior_map_edges",
809    "two_regime_half_split",
810]
@dataclass(frozen=True)
class CiScreenedPosterior:
183@dataclass(frozen=True)
184class CiScreenedPosterior:
185    alpha: float = 0.05
186    fdr: bool = True
187    ci: str = "parcorr"
188    max_cond_size: int = 2
189    soft_weight: Literal["none", "bayes_factor", "posterior_dependence"] = "none"
190    n_chains: int = 2
191    n_warmup: int = 300
192    n_draws: int = 600
193    thin: int = 1
194    kind: Literal["ci_screened_posterior"] = "ci_screened_posterior"
CiScreenedPosterior( alpha: float = 0.05, fdr: bool = True, ci: str = 'parcorr', max_cond_size: int = 2, soft_weight: "Literal['none', 'bayes_factor', 'posterior_dependence']" = 'none', n_chains: int = 2, n_warmup: int = 300, n_draws: int = 600, thin: int = 1, kind: "Literal['ci_screened_posterior']" = 'ci_screened_posterior')
alpha: float = 0.05
fdr: bool = True
ci: str = 'parcorr'
max_cond_size: int = 2
soft_weight: "Literal['none', 'bayes_factor', 'posterior_dependence']" = 'none'
n_chains: int = 2
n_warmup: int = 300
n_draws: int = 600
thin: int = 1
kind: "Literal['ci_screened_posterior']" = 'ci_screened_posterior'
@dataclass(frozen=True)
class DbnPosterior:
197@dataclass(frozen=True)
198class DbnPosterior:
199    """Bounded-lag DBN posterior (Gaussian BIC).
200
201    Exact enumeration only when ``p ≤ 4`` and ``max_lag ≤ 2``; larger templates
202    automatically use MCMC (or set ``force_mcmc=True``).
203
204    Standalone: ``discover_dbn_posterior(...)`` returns a ``GraphPosterior``.
205    Composed: pass ``discovery=DbnPosterior(...)`` with ``inference=Bayesian(...)``
206    and ``PulseEffect``/``SustainedEffect`` to mix temporal effect draws (P1-D).
207    """
208
209    max_lag: int = 1
210    force_mcmc: bool = False
211    n_chains: int = 2
212    n_warmup: int = 200
213    n_draws: int = 400
214    kind: Literal["dbn_posterior"] = "dbn_posterior"

Bounded-lag DBN posterior (Gaussian BIC).

Exact enumeration only when p ≤ 4 and max_lag ≤ 2; larger templates automatically use MCMC (or set force_mcmc=True).

Standalone: discover_dbn_posterior(...) returns a GraphPosterior. Composed: pass discovery=DbnPosterior(...) with inference=Bayesian(...) and PulseEffect/SustainedEffect to mix temporal effect draws (P1-D).

DbnPosterior( max_lag: int = 1, force_mcmc: bool = False, n_chains: int = 2, n_warmup: int = 200, n_draws: int = 400, kind: "Literal['dbn_posterior']" = 'dbn_posterior')
max_lag: int = 1
force_mcmc: bool = False
n_chains: int = 2
n_warmup: int = 200
n_draws: int = 400
kind: "Literal['dbn_posterior']" = 'dbn_posterior'
DiscoveryResult = <class 'builtins.PcmciDiscoveryResult'>
@dataclass(frozen=True)
class ExactDagPosterior:
150@dataclass(frozen=True)
151class ExactDagPosterior:
152    """Exact DAG posterior enumeration (hard limit: n ≤ 6, Gaussian BIC).
153
154    For more variables use ``OrderMcmc``, ``StructureMcmc``, or ``CiScreenedPosterior``.
155
156    Standalone: ``discover_exact_dag_posterior(...)`` returns a ``GraphPosterior``.
157    Composed: pass ``discovery=ExactDagPosterior()`` with ``inference=Bayesian(...)``
158    to ``analyze`` to mix effect draws over the graph posterior (P1-D).
159    """
160
161    kind: Literal["exact_dag_posterior"] = "exact_dag_posterior"

Exact DAG posterior enumeration (hard limit: n ≤ 6, Gaussian BIC).

For more variables use OrderMcmc, StructureMcmc, or CiScreenedPosterior.

Standalone: discover_exact_dag_posterior(...) returns a GraphPosterior. Composed: pass discovery=ExactDagPosterior() with inference=Bayesian(...) to analyze to mix effect draws over the graph posterior (P1-D).

ExactDagPosterior(kind: "Literal['exact_dag_posterior']" = 'exact_dag_posterior')
kind: "Literal['exact_dag_posterior']" = 'exact_dag_posterior'
@dataclass(frozen=True)
class FCI:
132@dataclass(frozen=True)
133class FCI:
134    alpha: float = 0.05
135    fdr: bool = True
136    ci: CiSpec = "parcorr"
137    max_cond_size: int = 2
138    kind: Literal["fci"] = "fci"
FCI( alpha: float = 0.05, fdr: bool = True, ci: 'CiSpec' = 'parcorr', max_cond_size: int = 2, kind: "Literal['fci']" = 'fci')
alpha: float = 0.05
fdr: bool = True
ci: 'CiSpec' = 'parcorr'
max_cond_size: int = 2
kind: "Literal['fci']" = 'fci'
@dataclass(frozen=True)
class GES:
105@dataclass(frozen=True)
106class GES:
107    alpha: float = 0.05
108    fdr: bool = True
109    ci: CiSpec = "parcorr"
110    max_cond_size: int = 2
111    screen_pc: bool = False
112    max_subset: int | None = None
113    kind: Literal["ges"] = "ges"
GES( alpha: float = 0.05, fdr: bool = True, ci: 'CiSpec' = 'parcorr', max_cond_size: int = 2, screen_pc: bool = False, max_subset: int | None = None, kind: "Literal['ges']" = 'ges')
alpha: float = 0.05
fdr: bool = True
ci: 'CiSpec' = 'parcorr'
max_cond_size: int = 2
screen_pc: bool = False
max_subset: int | None = None
kind: "Literal['ges']" = 'ges'
class GraphEdge:

One marked edge from an oriented temporal CPDAG/PAG.

at_source

Endpoint mark at source: tail | arrow | circle | conflict.

source_lag
target_lag
at_target

Endpoint mark at target: tail | arrow | circle | conflict.

target
source
class GraphPosterior:

Columnar graph posterior returned by Bayesian discovery engines.

def to_weighted_samples(self, /):

Summary dict compatible with weighted-graph envelope consumers.

def edge_marginal_matrix(self, /):

Edge marginal as an n_vars × n_vars nested list (row-major from→to).

weights
rejected_invalid
names
n_vars
max_lag
converged
lagged_edge_marginals
n_graphs
edge_marginals
orientation_marginals
ess
adjacency
@dataclass(frozen=True)
class JPCMCIPlus:
76@dataclass(frozen=True)
77class JPCMCIPlus:
78    max_lag: int = 1
79    alpha: float = 0.05
80    fdr: bool = True
81    ci: CiSpec = "parcorr"
82    context_names: tuple[str, ...] = ()
83    include_space_dummy: bool = True
84    include_time_dummy: bool = False
85    space_dummy_ci: Literal["scalar", "multivariate"] = "scalar"
86    time_dummy_encoding: Literal["integer", "one_hot"] = "integer"
87    time_dummy_ci: Literal["scalar", "multivariate"] = "scalar"
88    kind: Literal["jpcmci_plus"] = "jpcmci_plus"
JPCMCIPlus( max_lag: int = 1, alpha: float = 0.05, fdr: bool = True, ci: 'CiSpec' = 'parcorr', context_names: tuple[str, ...] = (), include_space_dummy: bool = True, include_time_dummy: bool = False, space_dummy_ci: "Literal['scalar', 'multivariate']" = 'scalar', time_dummy_encoding: "Literal['integer', 'one_hot']" = 'integer', time_dummy_ci: "Literal['scalar', 'multivariate']" = 'scalar', kind: "Literal['jpcmci_plus']" = 'jpcmci_plus')
max_lag: int = 1
alpha: float = 0.05
fdr: bool = True
ci: 'CiSpec' = 'parcorr'
context_names: tuple[str, ...] = ()
include_space_dummy: bool = True
include_time_dummy: bool = False
space_dummy_ci: "Literal['scalar', 'multivariate']" = 'scalar'
time_dummy_encoding: "Literal['integer', 'one_hot']" = 'integer'
time_dummy_ci: "Literal['scalar', 'multivariate']" = 'scalar'
kind: "Literal['jpcmci_plus']" = 'jpcmci_plus'
@dataclass(frozen=True)
class LPCMCI:
67@dataclass(frozen=True)
68class LPCMCI:
69    max_lag: int = 1
70    alpha: float = 0.05
71    fdr: bool = True
72    ci: CiSpec = "parcorr"
73    kind: Literal["lpcmci"] = "lpcmci"
LPCMCI( max_lag: int = 1, alpha: float = 0.05, fdr: bool = True, ci: 'CiSpec' = 'parcorr', kind: "Literal['lpcmci']" = 'lpcmci')
max_lag: int = 1
alpha: float = 0.05
fdr: bool = True
ci: 'CiSpec' = 'parcorr'
kind: "Literal['lpcmci']" = 'lpcmci'
@dataclass(frozen=True)
class LiNGAM:
116@dataclass(frozen=True)
117class LiNGAM:
118    prune_threshold: float = 0.05
119    max_cond_size: int = 8
120    kind: Literal["lingam"] = "lingam"
LiNGAM( prune_threshold: float = 0.05, max_cond_size: int = 8, kind: "Literal['lingam']" = 'lingam')
prune_threshold: float = 0.05
max_cond_size: int = 8
kind: "Literal['lingam']" = 'lingam'
@dataclass(frozen=True)
class NOTEARS:
123@dataclass(frozen=True)
124class NOTEARS:
125    l1: float = 0.1
126    threshold: float = 0.3
127    standardize: bool = True
128    max_cond_size: int = 8
129    kind: Literal["notears"] = "notears"
NOTEARS( l1: float = 0.1, threshold: float = 0.3, standardize: bool = True, max_cond_size: int = 8, kind: "Literal['notears']" = 'notears')
l1: float = 0.1
threshold: float = 0.3
standardize: bool = True
max_cond_size: int = 8
kind: "Literal['notears']" = 'notears'
@dataclass(frozen=True)
class OrderMcmc:
164@dataclass(frozen=True)
165class OrderMcmc:
166    n_chains: int = 4
167    n_warmup: int = 500
168    n_draws: int = 1000
169    thin: int = 1
170    require_diagnostics_gate: bool = True
171    kind: Literal["order_mcmc"] = "order_mcmc"
OrderMcmc( n_chains: int = 4, n_warmup: int = 500, n_draws: int = 1000, thin: int = 1, require_diagnostics_gate: bool = True, kind: "Literal['order_mcmc']" = 'order_mcmc')
n_chains: int = 4
n_warmup: int = 500
n_draws: int = 1000
thin: int = 1
require_diagnostics_gate: bool = True
kind: "Literal['order_mcmc']" = 'order_mcmc'
@dataclass(frozen=True)
class PC:
40@dataclass(frozen=True)
41class PC:
42    alpha: float = 0.05
43    fdr: bool = True
44    ci: CiSpec = "parcorr"
45    max_cond_size: int = 2
46    kind: Literal["pc"] = "pc"
PC( alpha: float = 0.05, fdr: bool = True, ci: 'CiSpec' = 'parcorr', max_cond_size: int = 2, kind: "Literal['pc']" = 'pc')
alpha: float = 0.05
fdr: bool = True
ci: 'CiSpec' = 'parcorr'
max_cond_size: int = 2
kind: "Literal['pc']" = 'pc'
@dataclass(frozen=True)
class PCMCI:
49@dataclass(frozen=True)
50class PCMCI:
51    max_lag: int = 1
52    alpha: float = 0.05
53    fdr: bool = True
54    ci: CiSpec = "parcorr"
55    kind: Literal["pcmci"] = "pcmci"
PCMCI( max_lag: int = 1, alpha: float = 0.05, fdr: bool = True, ci: 'CiSpec' = 'parcorr', kind: "Literal['pcmci']" = 'pcmci')
max_lag: int = 1
alpha: float = 0.05
fdr: bool = True
ci: 'CiSpec' = 'parcorr'
kind: "Literal['pcmci']" = 'pcmci'
@dataclass(frozen=True)
class PCMCIPlus:
58@dataclass(frozen=True)
59class PCMCIPlus:
60    max_lag: int = 1
61    alpha: float = 0.05
62    fdr: bool = True
63    ci: CiSpec = "parcorr"
64    kind: Literal["pcmci_plus"] = "pcmci_plus"
PCMCIPlus( max_lag: int = 1, alpha: float = 0.05, fdr: bool = True, ci: 'CiSpec' = 'parcorr', kind: "Literal['pcmci_plus']" = 'pcmci_plus')
max_lag: int = 1
alpha: float = 0.05
fdr: bool = True
ci: 'CiSpec' = 'parcorr'
kind: "Literal['pcmci_plus']" = 'pcmci_plus'
class PcmciDiscoveryResult:

Coarse-grained PCMCI discovery result (single boundary crossing).

Field set is the stable Rust↔Python temporal discovery schema for .

algorithm_id
ci_tests
worker_threads
algorithm_config
graph_edges

Oriented graph body (CPDAG/PAG marks); empty for lagged-only PCMCI.

ci_name
cpdag_nodes
cpdag_undirected_edges
cpdag_directed_edges
pending_edge_count
lagged_frame_bytes
@dataclass(frozen=True)
class RFCI:
141@dataclass(frozen=True)
142class RFCI:
143    alpha: float = 0.05
144    fdr: bool = True
145    ci: CiSpec = "parcorr"
146    max_cond_size: int = 2
147    kind: Literal["rfci"] = "rfci"
RFCI( alpha: float = 0.05, fdr: bool = True, ci: 'CiSpec' = 'parcorr', max_cond_size: int = 2, kind: "Literal['rfci']" = 'rfci')
alpha: float = 0.05
fdr: bool = True
ci: 'CiSpec' = 'parcorr'
max_cond_size: int = 2
kind: "Literal['rfci']" = 'rfci'
@dataclass(frozen=True)
class RPCMCI:
 91@dataclass(frozen=True)
 92class RPCMCI:
 93    """Regime-PCMCI. Pass ``regimes=`` to ``analyze`` / ``discover_rpcmci`` (required).
 94
 95    Use ``two_regime_half_split(n)`` when a simple half-split label vector is enough.
 96    """
 97
 98    max_lag: int = 1
 99    alpha: float = 0.05
100    fdr: bool = True
101    ci: CiSpec = "parcorr"
102    kind: Literal["rpcmci"] = "rpcmci"

Regime-PCMCI. Pass regimes= to analyze / discover_rpcmci (required).

Use two_regime_half_split(n) when a simple half-split label vector is enough.

RPCMCI( max_lag: int = 1, alpha: float = 0.05, fdr: bool = True, ci: 'CiSpec' = 'parcorr', kind: "Literal['rpcmci']" = 'rpcmci')
max_lag: int = 1
alpha: float = 0.05
fdr: bool = True
ci: 'CiSpec' = 'parcorr'
kind: "Literal['rpcmci']" = 'rpcmci'
class RpcmciDiscoverySummary:

RPCMCI summary (typed regimes, no single-graph collapse).

n_regimes
directed_edges
algorithm
undirected_edges
regime_ids
@dataclass(frozen=True)
class StructureMcmc:
174@dataclass(frozen=True)
175class StructureMcmc:
176    n_chains: int = 4
177    n_warmup: int = 500
178    n_draws: int = 1000
179    thin: int = 1
180    kind: Literal["structure_mcmc"] = "structure_mcmc"
StructureMcmc( n_chains: int = 4, n_warmup: int = 500, n_draws: int = 1000, thin: int = 1, kind: "Literal['structure_mcmc']" = 'structure_mcmc')
n_chains: int = 4
n_warmup: int = 500
n_draws: int = 1000
thin: int = 1
kind: "Literal['structure_mcmc']" = 'structure_mcmc'
def discover_ci_screened_posterior( names: 'Any | None' = None, columns: 'Sequence[NDArray[np.float64]] | None' = None, *, data: 'Any | None' = None, alpha: float = 0.05, fdr: bool = True, seed: int = 1, ci: str = 'parcorr', max_cond_size: int = 2, soft_weight: str = 'none', n_chains: int = 2, n_warmup: int = 300, n_draws: int = 600, thin: int = 1, threads: int = 1) -> GraphPosterior:
639def discover_ci_screened_posterior(
640    names: Any | None = None,
641    columns: Sequence[NDArray[np.float64]] | None = None,
642    *,
643    data: Any | None = None,
644    alpha: float = 0.05,
645    fdr: bool = True,
646    seed: int = 1,
647    ci: str = "parcorr",
648    max_cond_size: int = 2,
649    soft_weight: str = "none",
650    n_chains: int = 2,
651    n_warmup: int = 300,
652    n_draws: int = 600,
653    thin: int = 1,
654    threads: int = 1,
655) -> GraphPosterior:
656    n, cols = _coerce_tabular(names, columns, data=data)
657    return _discover_ci_screened_posterior(
658        n,
659        cols,
660        alpha=alpha,
661        fdr=fdr,
662        ci=ci,
663        max_cond_size=max_cond_size,
664        soft_weight=soft_weight,
665        n_chains=n_chains,
666        n_warmup=n_warmup,
667        n_draws=n_draws,
668        thin=thin,
669        seed=seed,
670        threads=threads,
671    )
def discover_dbn_posterior( names: 'Any | None' = None, columns: 'Sequence[NDArray[np.float64]] | None' = None, *, data: 'Any | None' = None, max_lag: int = 1, force_mcmc: bool = False, n_chains: int = 2, n_warmup: int = 200, n_draws: int = 400, seed: int = 1, threads: int = 1) -> GraphPosterior:
674def discover_dbn_posterior(
675    names: Any | None = None,
676    columns: Sequence[NDArray[np.float64]] | None = None,
677    *,
678    data: Any | None = None,
679    max_lag: int = 1,
680    force_mcmc: bool = False,
681    n_chains: int = 2,
682    n_warmup: int = 200,
683    n_draws: int = 400,
684    seed: int = 1,
685    threads: int = 1,
686) -> GraphPosterior:
687    """DBN template posterior; exact only for p ≤ 4 and max_lag ≤ 2, else MCMC."""
688    n, cols = _coerce_tabular(names, columns, data=data)
689    return _discover_dbn_posterior(
690        n,
691        cols,
692        max_lag=max_lag,
693        force_mcmc=force_mcmc,
694        n_chains=n_chains,
695        n_warmup=n_warmup,
696        n_draws=n_draws,
697        seed=seed,
698        threads=threads,
699    )

DBN template posterior; exact only for p ≤ 4 and max_lag ≤ 2, else MCMC.

def discover_exact_dag_posterior( names: 'Any | None' = None, columns: 'Sequence[NDArray[np.float64]] | None' = None, *, data: 'Any | None' = None, seed: int = 1, threads: int = 1) -> GraphPosterior:
574def discover_exact_dag_posterior(
575    names: Any | None = None,
576    columns: Sequence[NDArray[np.float64]] | None = None,
577    *,
578    data: Any | None = None,
579    seed: int = 1,
580    threads: int = 1,
581) -> GraphPosterior:
582    """Exact DAG posterior (hard limit n ≤ 6). Prefer MCMC helpers for larger graphs."""
583    n, cols = _coerce_tabular(names, columns, data=data)
584    return _discover_exact_dag_posterior(n, cols, seed=seed, threads=threads)

Exact DAG posterior (hard limit n ≤ 6). Prefer MCMC helpers for larger graphs.

def discover_jpcmci_plus( names: list[str], env_columns: 'Sequence[Sequence[NDArray[np.float64]]]', *, max_lag: int = 1, alpha: float = 0.05, fdr: bool = True, seed: int = 1, ci: str = 'parcorr', weights: list[float] | None = None, threads: int = 1, context_names: 'Sequence[str] | None' = None, include_space_dummy: bool = True, include_time_dummy: bool = False, space_dummy_ci: str = 'scalar', time_dummy_encoding: str = 'integer', time_dummy_ci: str = 'scalar') -> PcmciDiscoveryResult:
504def discover_jpcmci_plus(
505    names: list[str],
506    env_columns: Sequence[Sequence[NDArray[np.float64]]],
507    *,
508    max_lag: int = 1,
509    alpha: float = 0.05,
510    fdr: bool = True,
511    seed: int = 1,
512    ci: str = "parcorr",
513    weights: list[float] | None = None,
514    threads: int = 1,
515    context_names: Sequence[str] | None = None,
516    include_space_dummy: bool = True,
517    include_time_dummy: bool = False,
518    space_dummy_ci: str = "scalar",
519    time_dummy_encoding: str = "integer",
520    time_dummy_ci: str = "scalar",
521) -> PcmciDiscoveryResult:
522    return _discover_jpcmci_plus(
523        names,
524        [list(cols) for cols in env_columns],
525        max_lag=max_lag,
526        alpha=alpha,
527        fdr=fdr,
528        seed=seed,
529        ci=ci,
530        weights=weights,
531        threads=threads,
532        context_names=list(context_names) if context_names is not None else None,
533        include_space_dummy=include_space_dummy,
534        include_time_dummy=include_time_dummy,
535        space_dummy_ci=space_dummy_ci,
536        time_dummy_encoding=time_dummy_encoding,
537        time_dummy_ci=time_dummy_ci,
538    )
def discover_lpcmci( names: 'Any | None' = None, columns: 'Sequence[NDArray[np.float64]] | None' = None, *, data: 'Any | None' = None, max_lag: int = 1, alpha: float = 0.05, fdr: bool = True, seed: int = 1, ci: str = 'parcorr', weights: list[float] | None = None, threads: int = 1) -> PcmciDiscoveryResult:
477def discover_lpcmci(
478    names: Any | None = None,
479    columns: Sequence[NDArray[np.float64]] | None = None,
480    *,
481    data: Any | None = None,
482    max_lag: int = 1,
483    alpha: float = 0.05,
484    fdr: bool = True,
485    seed: int = 1,
486    ci: str = "parcorr",
487    weights: list[float] | None = None,
488    threads: int = 1,
489) -> DiscoveryResult:
490    n, cols = _coerce_tabular(names, columns, data=data)
491    return _discover_lpcmci(
492        n,
493        cols,
494        max_lag=max_lag,
495        alpha=alpha,
496        fdr=fdr,
497        seed=seed,
498        ci=ci,
499        weights=weights,
500        threads=threads,
501    )
def discover_order_mcmc( names: 'Any | None' = None, columns: 'Sequence[NDArray[np.float64]] | None' = None, *, data: 'Any | None' = None, n_chains: int = 4, n_warmup: int = 500, n_draws: int = 1000, thin: int = 1, require_diagnostics_gate: bool = True, seed: int = 1, threads: int = 1) -> GraphPosterior:
587def discover_order_mcmc(
588    names: Any | None = None,
589    columns: Sequence[NDArray[np.float64]] | None = None,
590    *,
591    data: Any | None = None,
592    n_chains: int = 4,
593    n_warmup: int = 500,
594    n_draws: int = 1000,
595    thin: int = 1,
596    require_diagnostics_gate: bool = True,
597    seed: int = 1,
598    threads: int = 1,
599) -> GraphPosterior:
600    n, cols = _coerce_tabular(names, columns, data=data)
601    return _discover_order_mcmc(
602        n,
603        cols,
604        n_chains=n_chains,
605        n_warmup=n_warmup,
606        n_draws=n_draws,
607        thin=thin,
608        require_diagnostics_gate=require_diagnostics_gate,
609        seed=seed,
610        threads=threads,
611    )
def discover_pc( names: 'Any | None' = None, columns: 'Sequence[NDArray[np.float64]] | None' = None, *, data: 'Any | None' = None, alpha: float = 0.05, fdr: bool = True, seed: int = 1, ci: str = 'parcorr', max_cond_size: int = 2, threads: int = 1) -> PcmciDiscoveryResult:
273def discover_pc(
274    names: Any | None = None,
275    columns: Sequence[NDArray[np.float64]] | None = None,
276    *,
277    data: Any | None = None,
278    alpha: float = 0.05,
279    fdr: bool = True,
280    seed: int = 1,
281    ci: str = "parcorr",
282    max_cond_size: int = 2,
283    threads: int = 1,
284) -> DiscoveryResult:
285    n, cols = _coerce_tabular(names, columns, data=data)
286    return _discover_pc(
287        n,
288        cols,
289        alpha=alpha,
290        fdr=fdr,
291        seed=seed,
292        ci=ci,
293        max_cond_size=max_cond_size,
294        threads=threads,
295    )
def discover_ges( names: 'Any | None' = None, columns: 'Sequence[NDArray[np.float64]] | None' = None, *, data: 'Any | None' = None, alpha: float = 0.05, fdr: bool = True, seed: int = 1, ci: str = 'parcorr', max_cond_size: int = 2, threads: int = 1, screen_pc: bool = False, max_subset: int | None = None) -> PcmciDiscoveryResult:
298def discover_ges(
299    names: Any | None = None,
300    columns: Sequence[NDArray[np.float64]] | None = None,
301    *,
302    data: Any | None = None,
303    alpha: float = 0.05,
304    fdr: bool = True,
305    seed: int = 1,
306    ci: str = "parcorr",
307    max_cond_size: int = 2,
308    threads: int = 1,
309    screen_pc: bool = False,
310    max_subset: int | None = None,
311) -> DiscoveryResult:
312    n, cols = _coerce_tabular(names, columns, data=data)
313    return _discover_ges(
314        n,
315        cols,
316        alpha=alpha,
317        fdr=fdr,
318        seed=seed,
319        ci=ci,
320        max_cond_size=max_cond_size,
321        threads=threads,
322        screen_pc=screen_pc,
323        max_subset=max_subset,
324    )
def discover_lingam( names: 'Any | None' = None, columns: 'Sequence[NDArray[np.float64]] | None' = None, *, data: 'Any | None' = None, prune_threshold: float = 0.05, seed: int = 1, max_cond_size: int = 8, threads: int = 1) -> PcmciDiscoveryResult:
327def discover_lingam(
328    names: Any | None = None,
329    columns: Sequence[NDArray[np.float64]] | None = None,
330    *,
331    data: Any | None = None,
332    prune_threshold: float = 0.05,
333    seed: int = 1,
334    max_cond_size: int = 8,
335    threads: int = 1,
336) -> DiscoveryResult:
337    n, cols = _coerce_tabular(names, columns, data=data)
338    return _discover_lingam(
339        n,
340        cols,
341        prune_threshold=prune_threshold,
342        seed=seed,
343        max_cond_size=max_cond_size,
344        threads=threads,
345    )
def discover_notears( names: 'Any | None' = None, columns: 'Sequence[NDArray[np.float64]] | None' = None, *, data: 'Any | None' = None, l1: float = 0.1, threshold: float = 0.3, standardize: bool = True, seed: int = 1, max_cond_size: int = 8, threads: int = 1) -> PcmciDiscoveryResult:
348def discover_notears(
349    names: Any | None = None,
350    columns: Sequence[NDArray[np.float64]] | None = None,
351    *,
352    data: Any | None = None,
353    l1: float = 0.1,
354    threshold: float = 0.3,
355    standardize: bool = True,
356    seed: int = 1,
357    max_cond_size: int = 8,
358    threads: int = 1,
359) -> DiscoveryResult:
360    n, cols = _coerce_tabular(names, columns, data=data)
361    return _discover_notears(
362        n,
363        cols,
364        l1=l1,
365        threshold=threshold,
366        standardize=standardize,
367        seed=seed,
368        max_cond_size=max_cond_size,
369        threads=threads,
370    )
def discover_fci( names: 'Any | None' = None, columns: 'Sequence[NDArray[np.float64]] | None' = None, *, data: 'Any | None' = None, alpha: float = 0.05, fdr: bool = True, seed: int = 1, ci: str = 'parcorr', max_cond_size: int = 2, threads: int = 1) -> PcmciDiscoveryResult:
373def discover_fci(
374    names: Any | None = None,
375    columns: Sequence[NDArray[np.float64]] | None = None,
376    *,
377    data: Any | None = None,
378    alpha: float = 0.05,
379    fdr: bool = True,
380    seed: int = 1,
381    ci: str = "parcorr",
382    max_cond_size: int = 2,
383    threads: int = 1,
384) -> DiscoveryResult:
385    n, cols = _coerce_tabular(names, columns, data=data)
386    return _discover_fci(
387        n,
388        cols,
389        alpha=alpha,
390        fdr=fdr,
391        seed=seed,
392        ci=ci,
393        max_cond_size=max_cond_size,
394        threads=threads,
395    )
def discover_rfci( names: 'Any | None' = None, columns: 'Sequence[NDArray[np.float64]] | None' = None, *, data: 'Any | None' = None, alpha: float = 0.05, fdr: bool = True, seed: int = 1, ci: str = 'parcorr', max_cond_size: int = 2, threads: int = 1) -> PcmciDiscoveryResult:
398def discover_rfci(
399    names: Any | None = None,
400    columns: Sequence[NDArray[np.float64]] | None = None,
401    *,
402    data: Any | None = None,
403    alpha: float = 0.05,
404    fdr: bool = True,
405    seed: int = 1,
406    ci: str = "parcorr",
407    max_cond_size: int = 2,
408    threads: int = 1,
409) -> DiscoveryResult:
410    n, cols = _coerce_tabular(names, columns, data=data)
411    return _discover_rfci(
412        n,
413        cols,
414        alpha=alpha,
415        fdr=fdr,
416        seed=seed,
417        ci=ci,
418        max_cond_size=max_cond_size,
419        threads=threads,
420    )
def discover_pcmci( names: 'Any | None' = None, columns: 'Sequence[NDArray[np.float64]] | None' = None, *, data: 'Any | None' = None, max_lag: int = 1, alpha: float = 0.05, fdr: bool = True, seed: int = 1, ci: str = 'parcorr', weights: list[float] | None = None, threads: int = 1) -> PcmciDiscoveryResult:
423def discover_pcmci(
424    names: Any | None = None,
425    columns: Sequence[NDArray[np.float64]] | None = None,
426    *,
427    data: Any | None = None,
428    max_lag: int = 1,
429    alpha: float = 0.05,
430    fdr: bool = True,
431    seed: int = 1,
432    ci: str = "parcorr",
433    weights: list[float] | None = None,
434    threads: int = 1,
435) -> DiscoveryResult:
436    n, cols = _coerce_tabular(names, columns, data=data)
437    return _discover_pcmci(
438        n,
439        cols,
440        max_lag=max_lag,
441        alpha=alpha,
442        fdr=fdr,
443        seed=seed,
444        ci=ci,
445        weights=weights,
446        threads=threads,
447    )
def discover_pcmci_plus( names: 'Any | None' = None, columns: 'Sequence[NDArray[np.float64]] | None' = None, *, data: 'Any | None' = None, max_lag: int = 1, alpha: float = 0.05, fdr: bool = True, seed: int = 1, ci: str = 'parcorr', weights: list[float] | None = None, threads: int = 1) -> PcmciDiscoveryResult:
450def discover_pcmci_plus(
451    names: Any | None = None,
452    columns: Sequence[NDArray[np.float64]] | None = None,
453    *,
454    data: Any | None = None,
455    max_lag: int = 1,
456    alpha: float = 0.05,
457    fdr: bool = True,
458    seed: int = 1,
459    ci: str = "parcorr",
460    weights: list[float] | None = None,
461    threads: int = 1,
462) -> DiscoveryResult:
463    n, cols = _coerce_tabular(names, columns, data=data)
464    return _discover_pcmci_plus(
465        n,
466        cols,
467        max_lag=max_lag,
468        alpha=alpha,
469        fdr=fdr,
470        seed=seed,
471        ci=ci,
472        weights=weights,
473        threads=threads,
474    )
def discover_rpcmci( names: 'Any | None' = None, columns: 'Sequence[NDArray[np.float64]] | None' = None, *, data: 'Any | None' = None, regimes: 'Sequence[int]', max_lag: int = 1, alpha: float = 0.05, fdr: bool = True, seed: int = 1, ci: str = 'parcorr', weights: list[float] | None = None, threads: int = 1) -> RpcmciDiscoverySummary:
541def discover_rpcmci(
542    names: Any | None = None,
543    columns: Sequence[NDArray[np.float64]] | None = None,
544    *,
545    data: Any | None = None,
546    regimes: Sequence[int],
547    max_lag: int = 1,
548    alpha: float = 0.05,
549    fdr: bool = True,
550    seed: int = 1,
551    ci: str = "parcorr",
552    weights: list[float] | None = None,
553    threads: int = 1,
554) -> RpcmciDiscoverySummary:
555    """Run RPCMCI. ``regimes`` is required (length = series length); no silent half-split.
556
557    Call ``two_regime_half_split(len(series))`` for an explicit two-regime mid-point split.
558    """
559    n, cols = _coerce_tabular(names, columns, data=data)
560    return _discover_rpcmci(
561        n,
562        cols,
563        regimes=list(regimes),
564        max_lag=max_lag,
565        alpha=alpha,
566        fdr=fdr,
567        seed=seed,
568        ci=ci,
569        weights=weights,
570        threads=threads,
571    )

Run RPCMCI. regimes is required (length = series length); no silent half-split.

Call two_regime_half_split(len(series)) for an explicit two-regime mid-point split.

def discover_structure_mcmc( names: 'Any | None' = None, columns: 'Sequence[NDArray[np.float64]] | None' = None, *, data: 'Any | None' = None, n_chains: int = 4, n_warmup: int = 500, n_draws: int = 1000, thin: int = 1, seed: int = 1, threads: int = 1) -> GraphPosterior:
614def discover_structure_mcmc(
615    names: Any | None = None,
616    columns: Sequence[NDArray[np.float64]] | None = None,
617    *,
618    data: Any | None = None,
619    n_chains: int = 4,
620    n_warmup: int = 500,
621    n_draws: int = 1000,
622    thin: int = 1,
623    seed: int = 1,
624    threads: int = 1,
625) -> GraphPosterior:
626    n, cols = _coerce_tabular(names, columns, data=data)
627    return _discover_structure_mcmc(
628        n,
629        cols,
630        n_chains=n_chains,
631        n_warmup=n_warmup,
632        n_draws=n_draws,
633        thin=thin,
634        seed=seed,
635        threads=threads,
636    )
def cpdag_oriented_edges( cpdag: "'Cpdag'", *, require_oriented: bool = True) -> list[tuple[str, str]]:
736def cpdag_oriented_edges(cpdag: "Cpdag", *, require_oriented: bool = True) -> list[tuple[str, str]]:
737    """Return directed edges from a CPDAG; error if undirected remain when required."""
738    from .graph import Cpdag
739
740    if not isinstance(cpdag, Cpdag):
741        raise TypeError(f"expected Cpdag, got {type(cpdag)!r}")
742    directed: list[tuple[str, str]] = []
743    undirected = 0
744    for src, tgt, kind in cpdag.edges():
745        if kind == "directed":
746            directed.append((src, tgt))
747        elif kind == "undirected":
748            undirected += 1
749        else:
750            undirected += 1
751    if undirected and require_oriented:
752        raise ValueError(
753            f"CPDAG has {undirected} undirected/ambiguous edge(s); orient before "
754            "PathSpecific/Interventional queries (require_oriented=True)"
755        )
756    if require_oriented:
757        try:
758            dag = cpdag.try_into_dag()
759        except Exception as exc:  # noqa: BLE001
760            raise ValueError(
761                "CPDAG is not fully oriented; cannot coerce to DAG for path/distribution queries"
762            ) from exc
763        return list(dag.edges())
764    return directed

Return directed edges from a CPDAG; error if undirected remain when required.

def discovery_to_dag(result: PcmciDiscoveryResult) -> "'Dag'":
221def discovery_to_dag(result: DiscoveryResult) -> "Dag":
222    """Build a ``Dag`` from a discovery result's directed ``graph_edges``.
223
224    Raises ``ValueError`` if any undirected/circle marks remain.
225    """
226    from .graph import Dag
227
228    names: list[str] = []
229    seen: set[str] = set()
230    directed: list[tuple[str, str]] = []
231    for e in result.graph_edges:
232        for n in (e.source, e.target):
233            if n not in seen:
234                seen.add(n)
235                names.append(n)
236        if e.at_source == "tail" and e.at_target == "arrow":
237            directed.append((e.source, e.target))
238        elif e.at_source == "arrow" and e.at_target == "tail":
239            directed.append((e.target, e.source))
240        else:
241            raise ValueError(
242                f"cannot coerce edge {e.source}->{e.target} "
243                f"({e.at_source}/{e.at_target}) into a DAG; "
244                "use graph_edges or a CPDAG/PAG constructor"
245            )
246    return Dag.from_edges(names, directed)

Build a Dag from a discovery result's directed graph_edges.

Raises ValueError if any undirected/circle marks remain.

def graph_posterior_map_dag(post: GraphPosterior) -> "'Dag'":
727def graph_posterior_map_dag(post: "GraphPosterior") -> "Dag":
728    """MAP DAG from a graph posterior (maximum-weight atom)."""
729    from .graph import Dag
730
731    edges = graph_posterior_map_edges(post)
732    names = list(post.names) if post.names else sorted({a for e in edges for a in e})
733    return Dag.from_edges(names, edges)

MAP DAG from a graph posterior (maximum-weight atom).

def graph_posterior_map_edges(post: GraphPosterior) -> list[tuple[str, str]]:
703def graph_posterior_map_edges(post: "GraphPosterior") -> list[tuple[str, str]]:
704    """Oriented edges from the maximum-weight adjacency mask in a graph posterior."""
705    import numpy as np
706
707    if post.n_graphs < 1 or not post.weights:
708        raise ValueError("GraphPosterior has no graphs")
709    i = int(np.argmax(np.asarray(post.weights, dtype=np.float64)))
710    mask = int(post.adjacency[i])
711    n = int(post.n_vars)
712    names = list(post.names)
713    if len(names) < n:
714        names = [f"x{j}" for j in range(n)]
715    edges: list[tuple[str, str]] = []
716    for fr in range(n):
717        for to in range(n):
718            if fr == to:
719                continue
720            # edge_bit(n, from, to) = from*(n-1) + (to if to < from else to-1)
721            bit = fr * (n - 1) + (to if to < fr else to - 1)
722            if (mask >> bit) & 1:
723                edges.append((names[fr], names[to]))
724    return edges

Oriented edges from the maximum-weight adjacency mask in a graph posterior.

def two_regime_half_split(series_len):

Two-regime half-split assignment (opt-in helper for RPCMCI; not applied by default).