Skip to content

X-13ARIMA-SEATS wrapper (tsecon.x13)

A Python wrapper around the U.S. Census Bureau's X-13ARIMA-SEATS seasonal-adjustment binary. Mirrors the Julia upstream's X13 module (TimeSeriesEcon.jl/src/x13/) API name-for-name where Python syntax allows.

The published wheels (M2.6 onward) vendor the X-13as binary inside src/tsecon/x13/_binary/{x13as, x13as.exe}pip install TimeSeriesEconPy produces an immediately usable wrapper with no further download step. Override the bundled binary by setting setoption("x13path", ...) to the absolute path of a locally installed x13as (useful for air-gapped sites, custom builds, or debugging across versions).

Quick start

import numpy as np
import tsecon

n = 100
start = tsecon.MIT.from_yp(tsecon.Quarterly(), 2000, 1)
rng = tsecon.MITRange(start, start + (n - 1))
i = np.arange(n)
ts = tsecon.TSeries(rng, 100.0 + 0.5 * i + 5.0 * np.sin(2 * np.pi * i / 4))

# One-call deseasonalisation: default x11 spec with `save = "d11"`.
adj = tsecon.x13.deseasonalize(ts)
print(adj[:8])
# → [100.001…, 100.666…, 101.000…, …]

For finer control, build a full X13spec via newspec and call run directly; the result exposes every parsed output table on result.series.* (and lazy proxies for tables not in the load= set).

Running, deseasonalisation, and cleanup

tsecon.x13._x13

Module-level conveniences for :mod:tsecon.x13.

Mirrors the convenience block of TimeSeriesEcon.jl/src/x13/X13.jl (X13.jl:42-98). The result-side types — :class:WorkspaceTable, :class:X13ResultWorkspace, :class:X13lazy, :class:X13result, :func:run, parsers — live in :mod:tsecon.x13._result. This sibling hosts the three top-level convenience functions:

  • :func:cleanup — sweep stale x13_* temp folders that the per-result :func:weakref.finalize callback missed (process hard kill, finalizer race at interpreter shutdown). Mirrors Julia cleanup at X13.jl:62-90.
  • :func:deseasonalize — run a default x11 spec on a TSeries and return a new TSeries with the d11 (final seasonally adjusted series) values. Mirrors Julia deseasonalize at X13.jl:58.
  • :func:deseasonalize_inplace — in-place flavour; replaces the values of the input TSeries in place. Mirrors Julia deseasonalize! at X13.jl:52-57. The _inplace rename follows the existing TimeSeriesEconPy convention (Julia foo! → Python foo_inplace) — see :func:tsecon.recursive.rec and :func:tsecon.workspace.merge_inplace for the parallel pattern.

The Julia upstream's module-level __init__ (X13.jl:92-98) prints an @info warning at import time when 5+ stale x13_* folders are found. Importing :mod:tsecon.x13 on every Python script start is already slower than Julia's using (no in-process module cache); we defer the leftover-folder count to the first :func:run call instead, where it's emitted as a :class:UserWarning rather than as a print statement.

get_cleanup_folders

get_cleanup_folders() -> list[str]

Return absolute paths of all x13_* folders in the system temp dir.

Mirrors Julia get_cleanup_folders at X13.jl:76-90. The Julia version filters by current-user UID; on POSIX we keep that check via :func:os.stat; on Windows we drop it (the system tempdir is per-user by default).

Source code in src/tsecon/x13/_x13.py
def get_cleanup_folders() -> list[str]:
    """Return absolute paths of all ``x13_*`` folders in the system temp dir.

    Mirrors Julia ``get_cleanup_folders`` at ``X13.jl:76-90``. The Julia
    version filters by current-user UID; on POSIX we keep that check
    via :func:`os.stat`; on Windows we drop it (the system tempdir is
    per-user by default).
    """
    parent = tempfile.gettempdir()
    out: list[str] = []
    try:
        entries = os.listdir(parent)
    except OSError:
        return out
    my_uid: int | None = None
    if hasattr(os, "geteuid"):
        try:
            my_uid = os.geteuid()
        except OSError:
            my_uid = None
    for name in entries:
        if not name.startswith(_TEMP_PREFIX):
            continue
        full = os.path.join(parent, name)
        if not os.path.isdir(full):
            continue
        if my_uid is not None:
            try:
                if os.stat(full).st_uid != my_uid:
                    continue
            except OSError:
                continue
        out.append(full)
    return out

cleanup

cleanup() -> None

Remove all stale x13_* folders from the system temp directory.

Mirrors Julia cleanup at X13.jl:62-74. Use this when a previous Python process was hard-killed before the per-result :func:weakref.finalize callbacks could fire, leaving X-13 temp folders behind. Emits a :class:UserWarning summarising the count removed (Julia prints to stdout; we use the warnings channel so the message routes through the project's filterwarnings config).

Source code in src/tsecon/x13/_x13.py
def cleanup() -> None:
    """Remove all stale ``x13_*`` folders from the system temp directory.

    Mirrors Julia ``cleanup`` at ``X13.jl:62-74``. Use this when a
    previous Python process was hard-killed before the per-result
    :func:`weakref.finalize` callbacks could fire, leaving X-13 temp
    folders behind. Emits a :class:`UserWarning` summarising the count
    removed (Julia prints to stdout; we use the warnings channel so
    the message routes through the project's ``filterwarnings`` config).
    """
    import shutil  # noqa: PLC0415 - keep import-time cost lean

    folders = get_cleanup_folders()
    for f in folders:
        shutil.rmtree(f, ignore_errors=True)
    warnings.warn(f"Removed {len(folders)} temporary x13 folders.", stacklevel=2)

deseasonalize_inplace

deseasonalize_inplace(
    ts: TSeries, **kwargs: Any
) -> TSeries

Run a default x11 spec on ts and replace its values with the d11 series.

Mirrors Julia deseasonalize! at X13.jl:52-57. The default seasonal-adjustment decomposition is multiplicative (mode="mult"); the seasonal filter defaults to the X-13 binary's automatic choice unless seasonalma= is passed. kwargs are forwarded to :func:tsecon.x13.x11 (the spec builder).

Returns ts for chaining (mirrors Julia's ts return).

Raises:

Type Description
RuntimeError

If no X-13 binary is available — the bundled binary lands in M2.6 (alongside the wheels matrix). Until then, point setoption("x13path", ...) at a user-installed binary.

Source code in src/tsecon/x13/_x13.py
def deseasonalize_inplace(ts: TSeries, **kwargs: Any) -> TSeries:
    """Run a default ``x11`` spec on ``ts`` and replace its values with the ``d11`` series.

    Mirrors Julia ``deseasonalize!`` at ``X13.jl:52-57``. The default
    seasonal-adjustment decomposition is multiplicative (``mode="mult"``);
    the seasonal filter defaults to the X-13 binary's automatic choice
    unless ``seasonalma=`` is passed. ``kwargs`` are forwarded to
    :func:`tsecon.x13.x11` (the spec builder).

    Returns ``ts`` for chaining (mirrors Julia's ``ts`` return).

    Raises
    ------
    RuntimeError
        If no X-13 binary is available — the bundled binary lands in
        M2.6 (alongside the wheels matrix). Until then, point
        ``setoption("x13path", ...)`` at a user-installed binary.
    """
    from tsecon.x13._spec import newspec, x11  # noqa: PLC0415 - avoid import cycle

    spec = newspec(ts, x11=x11(save="d11", **kwargs))
    res: X13result = run(spec, verbose=False)
    d11 = res.series.d11
    # d11 is a TSeries; copy its values into ``ts``'s buffer. The two
    # share a frequency by construction (the binary echoes the input's
    # frequency); guard against length mismatches in case the binary
    # produces a wider d11 (e.g. with forecasts appended).
    n = min(len(ts.values), len(d11.values))
    ts.values[:n] = d11.values[:n]
    return ts

deseasonalize

deseasonalize(ts: TSeries, **kwargs: Any) -> TSeries

Return a new TSeries with the X-11 seasonally adjusted values.

Mirrors Julia deseasonalize at X13.jl:58. Equivalent to :func:deseasonalize_inplace on a copy of the input.

Source code in src/tsecon/x13/_x13.py
def deseasonalize(ts: TSeries, **kwargs: Any) -> TSeries:
    """Return a new TSeries with the X-11 seasonally adjusted values.

    Mirrors Julia ``deseasonalize`` at ``X13.jl:58``. Equivalent to
    :func:`deseasonalize_inplace` on a copy of the input.
    """
    return deseasonalize_inplace(ts.copy(), **kwargs)

tsecon.x13._result.run

run(
    spec: X13spec | str,
    freq: Frequency | None = None,
    *,
    verbose: bool = True,
    allow_errors: bool = False,
    load: str | Sequence[str] = "none",
) -> X13result

Run X-13ARIMA-SEATS on a :class:X13spec or raw spec string.

Mirrors Julia X13.run (two methods at x13result.jl:76-88):

  • run(spec, *, ...) — pass an :class:X13spec; the spec is serialised via :func:tsecon.x13.x13write and written to <outfolder>/spec.spc.
  • run(specstring, freq, *, ...) — pass a pre-formatted .spc text plus the frequency the binary should assume; the spec text is written verbatim.

Parameters:

Name Type Description Default
spec X13spec | str

An :class:X13spec (the usual shape) or a raw spec string.

required
freq Frequency | None

Required when spec is a string; ignored otherwise.

None
verbose bool

Echo warnings / notes from the X-13 .err channel after the run finishes. Default :data:True.

True
allow_errors bool

If :data:False (default), raise :exc:RuntimeError when the X-13 binary reports errors. If :data:True, emit them as warnings and return the result anyway.

False
load str | Sequence[str]

Which output tables to eagerly parse. "none" (default) and "all" mirror Julia's :none / :all; otherwise pass a single key ("d11") or a sequence of keys (("d11", "d12")). Unrecognised keys are silently skipped (mirrors Julia intersect(_load, keys(res.series))).

'none'
Source code in src/tsecon/x13/_result.py
def run(
    spec: X13spec | str,
    freq: Frequency | None = None,
    *,
    verbose: bool = True,
    allow_errors: bool = False,
    load: str | Sequence[str] = "none",
) -> X13result:
    """Run X-13ARIMA-SEATS on a :class:`X13spec` or raw spec string.

    Mirrors Julia ``X13.run`` (two methods at ``x13result.jl:76-88``):

    * ``run(spec, *, ...)`` — pass an :class:`X13spec`; the spec is
      serialised via :func:`tsecon.x13.x13write` and written to
      ``<outfolder>/spec.spc``.
    * ``run(specstring, freq, *, ...)`` — pass a pre-formatted
      ``.spc`` text plus the frequency the binary should assume; the
      spec text is written verbatim.

    Parameters
    ----------
    spec
        An :class:`X13spec` (the usual shape) or a raw spec string.
    freq
        Required when ``spec`` is a string; ignored otherwise.
    verbose
        Echo warnings / notes from the X-13 ``.err`` channel after the
        run finishes. Default :data:`True`.
    allow_errors
        If :data:`False` (default), raise :exc:`RuntimeError` when the
        X-13 binary reports errors. If :data:`True`, emit them as
        warnings and return the result anyway.
    load
        Which output tables to eagerly parse. ``"none"`` (default) and
        ``"all"`` mirror Julia's ``:none`` / ``:all``; otherwise pass
        a single key (``"d11"``) or a sequence of keys
        (``("d11", "d12")``). Unrecognised keys are silently skipped
        (mirrors Julia ``intersect(_load, keys(res.series))``).
    """
    if isinstance(spec, str):
        if freq is None:
            msg = "run(specstring, freq=...): freq is required for the string overload."
            raise TypeError(msg)
        return _run_specstring(spec, freq, verbose=verbose, allow_errors=allow_errors, load=load)
    if not isinstance(spec, X13spec):
        msg = f"run() expects an X13spec or a spec string; got {type(spec).__name__}."  # type: ignore[unreachable]
        raise TypeError(msg)
    x13write(spec)
    if isinstance(spec.folder, X13default) or not spec.folder:
        spec.folder = tempfile.mkdtemp(prefix="x13_")
    folder = spec.folder
    assert isinstance(folder, str)
    Path(folder, "spec.spc").write_text(str(spec.string), encoding="utf-8")
    return _run_internal(spec, verbose=verbose, allow_errors=allow_errors, load=load)

tsecon.x13._result.X13result

Top-level container for one X-13 run's outputs.

Mirrors Julia X13result at x13result.jl:23-40. Construct via :func:run; direct construction is documented but considered internal.

Attributes:

Name Type Description
spec

The :class:X13spec that was run.

outfolder

Absolute path to the temp directory the run produced. Auto-removed when this :class:X13result is garbage-collected (via :func:weakref.finalize — see module docstring).

series

:class:X13ResultWorkspace of TSeries / MVTSeries outputs.

tables

:class:X13ResultWorkspace of :class:WorkspaceTable outputs (multi-column equal-length tables: acf, pcf, R/I/O, …).

text

:class:X13ResultWorkspace of plain-string outputs (the .out echo of the input spec, summaries, .err content).

other

:class:X13ResultWorkspace for everything else — UDG diagnostic codes, model / estimate / identify outputs.

stdout

The captured stdout from the X-13 binary.

errors / warnings / notes

Lists of strings parsed from the .err channel by :func:x13read_err.

Source code in src/tsecon/x13/_result.py
class X13result:
    """Top-level container for one X-13 run's outputs.

    Mirrors Julia ``X13result`` at ``x13result.jl:23-40``. Construct via
    :func:`run`; direct construction is documented but considered
    internal.

    Attributes
    ----------
    spec
        The :class:`X13spec` that was run.
    outfolder
        Absolute path to the temp directory the run produced.
        Auto-removed when this :class:`X13result` is garbage-collected
        (via :func:`weakref.finalize` — see module docstring).
    series
        :class:`X13ResultWorkspace` of TSeries / MVTSeries outputs.
    tables
        :class:`X13ResultWorkspace` of :class:`WorkspaceTable` outputs
        (multi-column equal-length tables: acf, pcf, R/I/O, …).
    text
        :class:`X13ResultWorkspace` of plain-string outputs (the
        ``.out`` echo of the input spec, summaries, ``.err`` content).
    other
        :class:`X13ResultWorkspace` for everything else — UDG diagnostic
        codes, model / estimate / identify outputs.
    stdout
        The captured stdout from the X-13 binary.
    errors / warnings / notes
        Lists of strings parsed from the ``.err`` channel by
        :func:`x13read_err`.
    """

    __slots__ = (
        "__weakref__",
        "_cleanup_handle",
        "errors",
        "notes",
        "other",
        "outfolder",
        "series",
        "spec",
        "stdout",
        "tables",
        "text",
        "warnings",
    )

    def __init__(self, spec: X13spec, outfolder: str, stdout: str) -> None:
        self.spec = spec
        self.outfolder = outfolder
        self.stdout = stdout
        self.series = X13ResultWorkspace()
        self.tables = X13ResultWorkspace()
        self.text = X13ResultWorkspace()
        self.other = X13ResultWorkspace()
        self.errors: list[str] = []
        self.warnings: list[str] = []
        self.notes: list[str] = []
        # Register the cleanup. The finalize callback only sees its args,
        # not the X13result instance, which is what allows GC of the
        # X13result to actually trigger the callback (a closure over self
        # would keep self alive forever).
        self._cleanup_handle = weakref.finalize(self, _cleanup_outfolder, outfolder)

    def __repr__(self) -> str:
        return _format_x13result(self)

    def __str__(self) -> str:
        return _format_x13result(self)

tsecon.x13._result.X13ResultWorkspace

Bases: Workspace

A :class:Workspace that materialises :class:X13lazy entries on first access.

Mirrors Julia X13ResultWorkspace at X13.jl:24-35 + Base.getproperty(::X13ResultWorkspace) at x13result.jl:49-56. Accessing an entry that is an :class:X13lazy triggers a call to :func:loadresult; the parsed object is written back into the underlying _c dict, so subsequent accesses skip the parse.

Both ws.foo (attribute) and ws["foo"] (key) access paths go through the same materialisation. Subset access (ws[["a", "b"]]) returns a plain :class:Workspace per the parent contract; lazy entries inside such a subset are not materialised eagerly.

Source code in src/tsecon/x13/_result.py
class X13ResultWorkspace(Workspace):
    """A :class:`Workspace` that materialises :class:`X13lazy` entries on first access.

    Mirrors Julia ``X13ResultWorkspace`` at ``X13.jl:24-35`` +
    ``Base.getproperty(::X13ResultWorkspace)`` at ``x13result.jl:49-56``.
    Accessing an entry that is an :class:`X13lazy` triggers a call to
    :func:`loadresult`; the parsed object is written back into the
    underlying ``_c`` dict, so subsequent accesses skip the parse.

    Both ``ws.foo`` (attribute) and ``ws["foo"]`` (key) access paths go
    through the same materialisation. Subset access (``ws[["a", "b"]]``)
    returns a plain :class:`Workspace` per the parent contract; lazy
    entries inside such a subset are *not* materialised eagerly.
    """

    __slots__ = ()

    def __getattr__(self, name: str) -> Any:
        # __getattr__ only fires when normal lookup fails; route to _c
        # ourselves rather than calling super().__getattr__ (which would
        # double-route through this method on subclasses).
        try:
            val = object.__getattribute__(self, "_c")[name]
        except KeyError as e:
            msg = f"X13ResultWorkspace has no member {name!r}"
            raise AttributeError(msg) from e
        if isinstance(val, X13lazy):
            val = loadresult(val)
            self._c[name] = val
        return val

    def __getitem__(self, key: Any) -> Any:
        val = super().__getitem__(key)
        # Subset access returns a Workspace, never an X13lazy.
        if isinstance(val, X13lazy) and isinstance(key, str):
            val = loadresult(val)
            self._c[key] = val
        return val

tsecon.x13._result.WorkspaceTable

Bases: Workspace

A :class:Workspace whose entries are equal-length vectors.

Mirrors Julia WorkspaceTable at X13.jl:12-22. The X-13 binary emits a number of multi-column tabular outputs (autocorrelation plots, sliding-spans summaries, R/I/O tables, …) where the natural Python representation is "a dict of equal-length lists". The underlying storage is the inherited _c dict; what differs is :meth:__repr__, which prints as a column-aligned table rather than the keyed list :class:Workspace produces.

Construction mirrors the parent: WorkspaceTable(), WorkspaceTable(a=[1, 2], b=[3, 4]), WorkspaceTable({"a": [1, 2]}). No validation runs at construction that columns have equal lengths — the X-13 readers always produce balanced columns, and unbalanced columns simply render with blank cells past their length (mirrors Julia).

Source code in src/tsecon/x13/_result.py
class WorkspaceTable(Workspace):
    """A :class:`Workspace` whose entries are equal-length vectors.

    Mirrors Julia ``WorkspaceTable`` at ``X13.jl:12-22``. The X-13 binary
    emits a number of multi-column tabular outputs (autocorrelation
    plots, sliding-spans summaries, R/I/O tables, …) where the natural
    Python representation is "a dict of equal-length lists". The
    underlying storage is the inherited ``_c`` dict; what differs is
    :meth:`__repr__`, which prints as a column-aligned table rather
    than the keyed list :class:`Workspace` produces.

    Construction mirrors the parent: ``WorkspaceTable()``,
    ``WorkspaceTable(a=[1, 2], b=[3, 4])``,
    ``WorkspaceTable({"a": [1, 2]})``. No validation runs at construction
    that columns have equal lengths — the X-13 readers always produce
    balanced columns, and unbalanced columns simply render with blank
    cells past their length (mirrors Julia).
    """

    __slots__ = ()

    def __repr__(self) -> str:
        return _format_workspace_table(self)

    def __str__(self) -> str:
        return _format_workspace_table(self)

tsecon.x13._result.X13lazy dataclass

A placeholder for an X-13 output that has not been parsed yet.

Mirrors Julia X13lazy at x13result.jl:43-47. Holds the path to the output file, the file extension (which selects the parser), and the data frequency (needed for :func:x13read_series / :func:x13read_seatsseries). :class:X13ResultWorkspace swaps the instance for the parsed object on first attribute / key access.

Source code in src/tsecon/x13/_result.py
@dataclass(frozen=True, slots=True)
class X13lazy:
    """A placeholder for an X-13 output that has not been parsed yet.

    Mirrors Julia ``X13lazy`` at ``x13result.jl:43-47``. Holds the path
    to the output file, the file extension (which selects the parser),
    and the data frequency (needed for :func:`x13read_series` /
    :func:`x13read_seatsseries`). :class:`X13ResultWorkspace` swaps the
    instance for the parsed object on first attribute / key access.
    """

    file: str
    ext: str
    frequency: Frequency

    def __repr__(self) -> str:
        return self.file

    def __str__(self) -> str:
        return self.file

tsecon.x13._result.loadresult

loadresult(
    lazy_or_file: X13lazy | str | PathLike[str],
    /,
    *args: Any,
) -> Any

Materialise an :class:X13lazy into the parsed object.

Mirrors Julia loadresult at x13result.jl:240-285. Two calling conventions:

  • loadresult(X13lazy(file, ext, freq)) — the usual shape, used by :class:X13ResultWorkspace when an attribute / key access hits a lazy entry.
  • loadresult(file, ext, freq) — same as above but with the three arguments passed positionally. Mirrors Julia's two-arity overload.
Source code in src/tsecon/x13/_result.py
def loadresult(lazy_or_file: X13lazy | str | os.PathLike[str], /, *args: Any) -> Any:
    """Materialise an :class:`X13lazy` into the parsed object.

    Mirrors Julia ``loadresult`` at ``x13result.jl:240-285``. Two
    calling conventions:

    * ``loadresult(X13lazy(file, ext, freq))`` — the usual shape, used
      by :class:`X13ResultWorkspace` when an attribute / key access
      hits a lazy entry.
    * ``loadresult(file, ext, freq)`` — same as above but with the
      three arguments passed positionally. Mirrors Julia's two-arity
      overload.
    """
    if isinstance(lazy_or_file, X13lazy):
        file = lazy_or_file.file
        ext = lazy_or_file.ext
        freq = lazy_or_file.frequency
    else:
        file = os.fspath(lazy_or_file)
        if len(args) != 2:
            msg = "loadresult(file, ext, freq) requires three positional arguments."
            raise TypeError(msg)
        ext, freq = args
    return _dispatch_loadresult(file, ext, freq)

Spec construction

The aggregator (X13spec), the factory (newspec), and the validator (validateX13spec) form the top-level spec API; the 19 builders below populate the named subspec fields on an X13spec instance.

tsecon.x13._spec

X-13ARIMA-SEATS spec types and spec builders.

Mirrors TimeSeriesEcon.jl/src/x13/x13spec.jl (4,137 LOC — the bulk of M2). Lands across M2.1 / M2.2 / M2.3 / M2.4.

M2.1 — :class:X13var dataclasses + ARIMA-spec types (this session)

This module ships:

  • :class:X13default sentinel + module-private :data:_X13DEFAULT singleton.
  • :class:RegimeChange :class:~enum.StrEnum for the trading-day / seasonal regime-change qualifier (both / zerobefore / zeroafter / neither).
  • :class:X13var abstract base + 26 concrete leaves the spec builders accept as outlier / regressor arguments. Each __str__ produces the X13as .spc-grammar token; pickling and equality come from @dataclass(frozen=True, slots=True).

  • Point outliers (mit: MIT): ao (x13spec.jl:9-11), ls (20-22), tc (31-33), so (35-37).

  • Range outliers (mit1, mit2: MIT; classmethod :meth:~aos.from_range): aos (12-18), lss (23-29), rp (39-45), qd (47-53), qi (54-60), tl (62-68).
  • Trading-day regressors with optional regime change (mit: MIT | None, regimechange: RegimeChange): td (96-103), tdnolpyear (104-111), td1coef (112-119), td1nolpyear (120-127).
  • Trading-day calendar (n: int): tdstock (70-72), tdstock1coef (73-75).
  • Calendar regressors (n: int): easter (76-78), labor (79-81), thank (82-84), sceaster (86-88), easterstock (89-91).
  • Calendar regressor (n: tuple[int, ...]): sincos (92-94).
  • Length-of-period / leap-year (mit: MIT | None, regimechange): lpyear (129-135), lom (137-143), loq (146-152).
  • Seasonal regressor (mit, regimechange): seasonal (154-161).

The Julia upstream's :func:ao etc. are types whose lowercase names are the X13as keywords. The Python port preserves the lowercase names (with a per-file N801 lint ignore in pyproject.toml) because the __str__ output uses type(self).__name__ — capitalising the classes would break .spc serialization or require a parallel name map.

The Julia source has 26 X13var types.

  • :class:ArimaSpec and :class:ArimaModel — the generic (p, d, q)(P, D, Q)[period] ARIMA builders the :func:arima / :func:pickmdl spec builders (M2.2 / M2.3) consume.

M2.2 — High-traffic spec builders (later session)

8 builders that every realistic spec uses: :func:series (x13spec.jl:732); :func:x11 (3180), :func:seats (2478); :func:arima (873), :func:automdl (1034); :func:transform (2928), :func:regression (2219); :func:forecast (1409).

M2.3 — Rare spec builders (later session)

11 builders that round out parity: :func:outlier (1833), :func:history (1602), :func:identify (1686), :func:check (1149), :func:estimate (1228), :func:metadata (1721), :func:pickmdl (1972), :func:force (1343), :func:slidingspans (2666), :func:spectrum (2785), :func:x11regression (3420).

M2.4 — :class:X13spec container + validation (later session)

:class:X13spec (x13spec.jl:4138) is the per-frequency typed container the builders accumulate into. :func:newspec (578) / :func:newspec_from_frequency (603) are the constructors; :func:validateX13spec (3563) is the cross-builder invariant check that runs before :func:run ships the spec to the binary.

Notes on Python idiom (M2.1)

The Julia upstream's dual-constructor for range outliers (aos(::UnitRange{<:MIT}) vs aos(mit1::MIT, mit2::MIT)) ports as the canonical two-argument constructor plus an :meth:~aos.from_range :func:classmethod accepting an :class:~tsecon.mitrange.MITRange. This keeps the frozen-slots dataclass shape uniform and matches how :class:MIT itself surfaces alternate constructors (MIT.from_yp).

The Julia regimechange::Symbol field ports as a :class:RegimeChange :class:~enum.StrEnum; users may pass either the enum member (RegimeChange.BOTH) or the bare string ("both"), the :meth:__post_init__ normalises to the enum. Members are equal to their .value (RegimeChange.BOTH == "both" is :data:True) so equality checks remain transparent.

The Julia td() / td(mit::MIT) / td(mit, rc) constructor trio maps to a single Python dataclass with sentinel defaults (mit=None, regimechange=None); :meth:__post_init__ resolves (None, None) to regimechange=NEITHER (no-arg form) and (mit, None) to regimechange=BOTH (mit-only form, matching Julia's default). The same pattern applies to the seven other regime-bearing types (tdnolpyear, td1coef, td1nolpyear, lpyear, lom, loq, seasonal).

X13default

Sentinel for spec-builder fields that should be left at the X-13 default.

Mirrors Julia's X13default struct (x13spec.jl:4). Spec builders (M2.2 / M2.3) accept :class:X13default instances as default arguments; the .spc writer (M2.4) skips fields whose value is _X13DEFAULT, leaving the binary to apply its built-in default.

The class is a singleton — :data:_X13DEFAULT is the canonical instance, and identity comparisons (arg is _X13DEFAULT) replace Julia's isa X13default dispatch.

Source code in src/tsecon/x13/_spec.py
class X13default:
    """Sentinel for spec-builder fields that should be left at the X-13 default.

    Mirrors Julia's ``X13default`` struct (``x13spec.jl:4``). Spec builders
    (M2.2 / M2.3) accept :class:`X13default` instances as default arguments;
    the ``.spc`` writer (M2.4) skips fields whose value ``is _X13DEFAULT``,
    leaving the binary to apply its built-in default.

    The class is a singleton — :data:`_X13DEFAULT` is the canonical instance,
    and identity comparisons (``arg is _X13DEFAULT``) replace Julia's
    ``isa X13default`` dispatch.
    """

    __slots__ = ()
    _instance: ClassVar[X13default | None] = None

    def __new__(cls) -> X13default:
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __repr__(self) -> str:
        return "X13default()"

RegimeChange

Bases: StrEnum

Regime-change qualifier for td*, lpyear, lom, loq, seasonal.

Maps to X-13ARIMA-SEATS .spc tokens:

  • :attr:NEITHER — no regime change; the MIT is unused in serialization.
  • :attr:BOTH — break at mit, both halves estimated. Token: td/2020.jul/.
  • :attr:ZEROBEFORE — break with zero coefficient before mit. Token: td//2020.jul/.
  • :attr:ZEROAFTER — break with zero coefficient after mit. Token: td/2020.jul//.

:class:RegimeChange is a :class:~enum.StrEnum; members compare equal to their underlying string (RegimeChange.BOTH == "both"). Spec constructors normalise plain strings to enum members via :meth:__post_init__.

Source code in src/tsecon/x13/_spec.py
class RegimeChange(StrEnum):
    """Regime-change qualifier for ``td*``, ``lpyear``, ``lom``, ``loq``, ``seasonal``.

    Maps to X-13ARIMA-SEATS ``.spc`` tokens:

    * :attr:`NEITHER` — no regime change; the MIT is unused in serialization.
    * :attr:`BOTH` — break at ``mit``, both halves estimated.
      Token: ``td/2020.jul/``.
    * :attr:`ZEROBEFORE` — break with zero coefficient before ``mit``.
      Token: ``td//2020.jul/``.
    * :attr:`ZEROAFTER` — break with zero coefficient after ``mit``.
      Token: ``td/2020.jul//``.

    :class:`RegimeChange` is a :class:`~enum.StrEnum`; members compare equal
    to their underlying string (``RegimeChange.BOTH == "both"``). Spec
    constructors normalise plain strings to enum members via
    :meth:`__post_init__`.
    """

    BOTH = "both"
    ZEROBEFORE = "zerobefore"
    ZEROAFTER = "zeroafter"
    NEITHER = "neither"

X13var

Abstract base class for X-13 outlier / regressor / calendar variables.

Each concrete subclass is a frozen-slotted dataclass that overrides :meth:__str__ to produce the .spc-grammar token. Subclasses use lowercase Julia-mirror names (ao, ls, …); the class name is consumed by serialization via type(self).__name__ — capitalising it would either break .spc output or require a parallel name map.

Source code in src/tsecon/x13/_spec.py
class X13var:
    """Abstract base class for X-13 outlier / regressor / calendar variables.

    Each concrete subclass is a frozen-slotted dataclass that overrides
    :meth:`__str__` to produce the ``.spc``-grammar token. Subclasses use
    lowercase Julia-mirror names (``ao``, ``ls``, …); the class name is
    consumed by serialization via ``type(self).__name__`` — capitalising
    it would either break ``.spc`` output or require a parallel name map.
    """

    __slots__ = ()

ao dataclass

Bases: X13var

Additive outlier at mit (regARIMA AO regressor).

Mirrors x13spec.jl:9-11. Serializes as ao<year>.<period>, e.g. ao2020.jul for Monthly or ao2020.3 for Quarterly.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class ao(X13var):
    """Additive outlier at ``mit`` (regARIMA AO regressor).

    Mirrors ``x13spec.jl:9-11``. Serializes as ``ao<year>.<period>``,
    e.g. ``ao2020.jul`` for Monthly or ``ao2020.3`` for Quarterly.
    """

    mit: MIT

    def __str__(self) -> str:
        return f"ao{_mit_to_spc(self.mit)}"

ls dataclass

Bases: X13var

Level-shift outlier at mit.

Mirrors x13spec.jl:20-22. Serializes as ls<year>.<period>.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class ls(X13var):
    """Level-shift outlier at ``mit``.

    Mirrors ``x13spec.jl:20-22``. Serializes as ``ls<year>.<period>``.
    """

    mit: MIT

    def __str__(self) -> str:
        return f"ls{_mit_to_spc(self.mit)}"

tc dataclass

Bases: X13var

Temporary-change outlier at mit.

Mirrors x13spec.jl:31-33. Serializes as tc<year>.<period>.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class tc(X13var):
    """Temporary-change outlier at ``mit``.

    Mirrors ``x13spec.jl:31-33``. Serializes as ``tc<year>.<period>``.
    """

    mit: MIT

    def __str__(self) -> str:
        return f"tc{_mit_to_spc(self.mit)}"

so dataclass

Bases: X13var

Seasonal outlier at mit.

Mirrors x13spec.jl:35-37. Serializes as so<year>.<period>.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class so(X13var):
    """Seasonal outlier at ``mit``.

    Mirrors ``x13spec.jl:35-37``. Serializes as ``so<year>.<period>``.
    """

    mit: MIT

    def __str__(self) -> str:
        return f"so{_mit_to_spc(self.mit)}"

aos dataclass

Bases: X13var

Range additive-outlier from mit1 to mit2 (inclusive).

Mirrors x13spec.jl:12-18. Serializes as aos<year1>.<period1>-<year2>.<period2>. Construct with :meth:from_range to bridge an :class:~tsecon.mitrange.MITRange.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class aos(X13var):
    """Range additive-outlier from ``mit1`` to ``mit2`` (inclusive).

    Mirrors ``x13spec.jl:12-18``. Serializes as
    ``aos<year1>.<period1>-<year2>.<period2>``. Construct with
    :meth:`from_range` to bridge an :class:`~tsecon.mitrange.MITRange`.
    """

    mit1: MIT
    mit2: MIT

    @classmethod
    def from_range(cls, mr: MITRange) -> aos:
        """Construct from an :class:`~tsecon.mitrange.MITRange`.

        Mirrors the Julia inner constructor
        ``aos(x::UnitRange{<:MIT}) = new(first(x), last(x))``. The Python
        :class:`~tsecon.mitrange.MITRange` is inclusive, so its
        :meth:`~tsecon.mitrange.MITRange.first` and
        :meth:`~tsecon.mitrange.MITRange.last` match the Julia range
        endpoints.
        """
        return cls(mr.first(), mr.last())

    def __str__(self) -> str:
        return f"aos{_mit_to_spc(self.mit1)}-{_mit_to_spc(self.mit2)}"

from_range classmethod

from_range(mr: MITRange) -> aos

Construct from an :class:~tsecon.mitrange.MITRange.

Mirrors the Julia inner constructor aos(x::UnitRange{<:MIT}) = new(first(x), last(x)). The Python :class:~tsecon.mitrange.MITRange is inclusive, so its :meth:~tsecon.mitrange.MITRange.first and :meth:~tsecon.mitrange.MITRange.last match the Julia range endpoints.

Source code in src/tsecon/x13/_spec.py
@classmethod
def from_range(cls, mr: MITRange) -> aos:
    """Construct from an :class:`~tsecon.mitrange.MITRange`.

    Mirrors the Julia inner constructor
    ``aos(x::UnitRange{<:MIT}) = new(first(x), last(x))``. The Python
    :class:`~tsecon.mitrange.MITRange` is inclusive, so its
    :meth:`~tsecon.mitrange.MITRange.first` and
    :meth:`~tsecon.mitrange.MITRange.last` match the Julia range
    endpoints.
    """
    return cls(mr.first(), mr.last())

lss dataclass

Bases: X13var

Range level-shift outlier from mit1 to mit2.

Mirrors x13spec.jl:23-29.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class lss(X13var):
    """Range level-shift outlier from ``mit1`` to ``mit2``.

    Mirrors ``x13spec.jl:23-29``.
    """

    mit1: MIT
    mit2: MIT

    @classmethod
    def from_range(cls, mr: MITRange) -> lss:
        """Construct from an :class:`~tsecon.mitrange.MITRange`."""
        return cls(mr.first(), mr.last())

    def __str__(self) -> str:
        return f"lss{_mit_to_spc(self.mit1)}-{_mit_to_spc(self.mit2)}"

from_range classmethod

from_range(mr: MITRange) -> lss

Construct from an :class:~tsecon.mitrange.MITRange.

Source code in src/tsecon/x13/_spec.py
@classmethod
def from_range(cls, mr: MITRange) -> lss:
    """Construct from an :class:`~tsecon.mitrange.MITRange`."""
    return cls(mr.first(), mr.last())

rp dataclass

Bases: X13var

Ramp outlier from mit1 to mit2.

Mirrors x13spec.jl:39-45.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class rp(X13var):
    """Ramp outlier from ``mit1`` to ``mit2``.

    Mirrors ``x13spec.jl:39-45``.
    """

    mit1: MIT
    mit2: MIT

    @classmethod
    def from_range(cls, mr: MITRange) -> rp:
        """Construct from an :class:`~tsecon.mitrange.MITRange`."""
        return cls(mr.first(), mr.last())

    def __str__(self) -> str:
        return f"rp{_mit_to_spc(self.mit1)}-{_mit_to_spc(self.mit2)}"

from_range classmethod

from_range(mr: MITRange) -> rp

Construct from an :class:~tsecon.mitrange.MITRange.

Source code in src/tsecon/x13/_spec.py
@classmethod
def from_range(cls, mr: MITRange) -> rp:
    """Construct from an :class:`~tsecon.mitrange.MITRange`."""
    return cls(mr.first(), mr.last())

qd dataclass

Bases: X13var

Quadratic-decay outlier range from mit1 to mit2.

Mirrors x13spec.jl:47-53.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class qd(X13var):
    """Quadratic-decay outlier range from ``mit1`` to ``mit2``.

    Mirrors ``x13spec.jl:47-53``.
    """

    mit1: MIT
    mit2: MIT

    @classmethod
    def from_range(cls, mr: MITRange) -> qd:
        """Construct from an :class:`~tsecon.mitrange.MITRange`."""
        return cls(mr.first(), mr.last())

    def __str__(self) -> str:
        return f"qd{_mit_to_spc(self.mit1)}-{_mit_to_spc(self.mit2)}"

from_range classmethod

from_range(mr: MITRange) -> qd

Construct from an :class:~tsecon.mitrange.MITRange.

Source code in src/tsecon/x13/_spec.py
@classmethod
def from_range(cls, mr: MITRange) -> qd:
    """Construct from an :class:`~tsecon.mitrange.MITRange`."""
    return cls(mr.first(), mr.last())

qi dataclass

Bases: X13var

Quadratic-incline outlier range from mit1 to mit2.

Mirrors x13spec.jl:54-60.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class qi(X13var):
    """Quadratic-incline outlier range from ``mit1`` to ``mit2``.

    Mirrors ``x13spec.jl:54-60``.
    """

    mit1: MIT
    mit2: MIT

    @classmethod
    def from_range(cls, mr: MITRange) -> qi:
        """Construct from an :class:`~tsecon.mitrange.MITRange`."""
        return cls(mr.first(), mr.last())

    def __str__(self) -> str:
        return f"qi{_mit_to_spc(self.mit1)}-{_mit_to_spc(self.mit2)}"

from_range classmethod

from_range(mr: MITRange) -> qi

Construct from an :class:~tsecon.mitrange.MITRange.

Source code in src/tsecon/x13/_spec.py
@classmethod
def from_range(cls, mr: MITRange) -> qi:
    """Construct from an :class:`~tsecon.mitrange.MITRange`."""
    return cls(mr.first(), mr.last())

tl dataclass

Bases: X13var

Temporary-level-shift outlier range from mit1 to mit2.

Mirrors x13spec.jl:62-68.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class tl(X13var):
    """Temporary-level-shift outlier range from ``mit1`` to ``mit2``.

    Mirrors ``x13spec.jl:62-68``.
    """

    mit1: MIT
    mit2: MIT

    @classmethod
    def from_range(cls, mr: MITRange) -> tl:
        """Construct from an :class:`~tsecon.mitrange.MITRange`."""
        return cls(mr.first(), mr.last())

    def __str__(self) -> str:
        return f"tl{_mit_to_spc(self.mit1)}-{_mit_to_spc(self.mit2)}"

from_range classmethod

from_range(mr: MITRange) -> tl

Construct from an :class:~tsecon.mitrange.MITRange.

Source code in src/tsecon/x13/_spec.py
@classmethod
def from_range(cls, mr: MITRange) -> tl:
    """Construct from an :class:`~tsecon.mitrange.MITRange`."""
    return cls(mr.first(), mr.last())

tdstock dataclass

Bases: X13var

Trading-day stock regressor with calendar-day-of-month n.

Mirrors x13spec.jl:70-72. Serializes as tdstock[n].

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class tdstock(X13var):
    """Trading-day stock regressor with calendar-day-of-month ``n``.

    Mirrors ``x13spec.jl:70-72``. Serializes as ``tdstock[n]``.
    """

    n: int

    def __str__(self) -> str:
        return f"tdstock[{self.n}]"

tdstock1coef dataclass

Bases: X13var

One-coefficient trading-day stock regressor with day n.

Mirrors x13spec.jl:73-75.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class tdstock1coef(X13var):
    """One-coefficient trading-day stock regressor with day ``n``.

    Mirrors ``x13spec.jl:73-75``.
    """

    n: int

    def __str__(self) -> str:
        return f"tdstock1coef[{self.n}]"

easter dataclass

Bases: X13var

Easter holiday regressor with window length n days.

Mirrors x13spec.jl:76-78.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class easter(X13var):
    """Easter holiday regressor with window length ``n`` days.

    Mirrors ``x13spec.jl:76-78``.
    """

    n: int

    def __str__(self) -> str:
        return f"easter[{self.n}]"

labor dataclass

Bases: X13var

U.S. Labor Day regressor with window length n days.

Mirrors x13spec.jl:79-81.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class labor(X13var):
    """U.S. Labor Day regressor with window length ``n`` days.

    Mirrors ``x13spec.jl:79-81``.
    """

    n: int

    def __str__(self) -> str:
        return f"labor[{self.n}]"

thank dataclass

Bases: X13var

U.S. Thanksgiving regressor with window length n days.

Mirrors x13spec.jl:82-84.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class thank(X13var):
    """U.S. Thanksgiving regressor with window length ``n`` days.

    Mirrors ``x13spec.jl:82-84``.
    """

    n: int

    def __str__(self) -> str:
        return f"thank[{self.n}]"

sceaster dataclass

Bases: X13var

Statistics-Canada Easter regressor with window length n days.

Mirrors x13spec.jl:86-88.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class sceaster(X13var):
    """Statistics-Canada Easter regressor with window length ``n`` days.

    Mirrors ``x13spec.jl:86-88``.
    """

    n: int

    def __str__(self) -> str:
        return f"sceaster[{self.n}]"

easterstock dataclass

Bases: X13var

Easter-stock regressor with window length n days.

Mirrors x13spec.jl:89-91.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class easterstock(X13var):
    """Easter-stock regressor with window length ``n`` days.

    Mirrors ``x13spec.jl:89-91``.
    """

    n: int

    def __str__(self) -> str:
        return f"easterstock[{self.n}]"

sincos dataclass

Bases: X13var

Sine/cosine seasonal-frequency regressor with frequencies n.

Mirrors x13spec.jl:92-94. Julia uses n::Vector{Int64}; the Python port uses tuple[int, ...] so the dataclass stays frozen. Serializes as sincos[<space-separated frequencies>].

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class sincos(X13var):
    """Sine/cosine seasonal-frequency regressor with frequencies ``n``.

    Mirrors ``x13spec.jl:92-94``. Julia uses ``n::Vector{Int64}``; the
    Python port uses ``tuple[int, ...]`` so the dataclass stays frozen.
    Serializes as ``sincos[<space-separated frequencies>]``.
    """

    n: tuple[int, ...]

    def __post_init__(self) -> None:
        # Runtime check guards against users passing a list (the
        # Julia-flavored form); the type annotation says tuple, so mypy
        # marks the error path unreachable — that's by design.
        if not isinstance(self.n, tuple):
            msg = (  # type: ignore[unreachable]
                f"sincos.n must be a tuple of ints, got "
                f"{type(self.n).__name__}; pass tuple(...) explicitly."
            )
            raise TypeError(msg)

    def __str__(self) -> str:
        return f"sincos[{' '.join(str(k) for k in self.n)}]"

td dataclass

Bases: X13var

Trading-day regressor with optional regime change at mit.

Mirrors x13spec.jl:96-103. Three call shapes mirror the Julia upstream:

  • td() — no regime change. .spc form: td.
  • td(mit) — regime change at mit, both halves estimated. .spc form: td/<mit>/.
  • td(mit, regimechange=...) — explicit regime-change qualifier.
Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class td(X13var):
    """Trading-day regressor with optional regime change at ``mit``.

    Mirrors ``x13spec.jl:96-103``. Three call shapes mirror the Julia
    upstream:

    * ``td()`` — no regime change. .spc form: ``td``.
    * ``td(mit)`` — regime change at ``mit``, both halves estimated. .spc
      form: ``td/<mit>/``.
    * ``td(mit, regimechange=...)`` — explicit regime-change qualifier.
    """

    mit: MIT | None = None
    regimechange: RegimeChange = RegimeChange.NEITHER

    def __init__(
        self,
        mit: MIT | None = None,
        regimechange: _RegimeChangeArg = None,
    ) -> None:
        resolved = _resolve_regime(mit, regimechange)
        object.__setattr__(self, "mit", mit)
        object.__setattr__(self, "regimechange", resolved)

    def __str__(self) -> str:
        return _regime_str("td", self.mit, self.regimechange)

tdnolpyear dataclass

Bases: X13var

Trading-day regressor (no leap-year term) with optional regime change.

Mirrors x13spec.jl:104-111.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class tdnolpyear(X13var):
    """Trading-day regressor (no leap-year term) with optional regime change.

    Mirrors ``x13spec.jl:104-111``.
    """

    mit: MIT | None = None
    regimechange: RegimeChange = RegimeChange.NEITHER

    def __init__(
        self,
        mit: MIT | None = None,
        regimechange: _RegimeChangeArg = None,
    ) -> None:
        resolved = _resolve_regime(mit, regimechange)
        object.__setattr__(self, "mit", mit)
        object.__setattr__(self, "regimechange", resolved)

    def __str__(self) -> str:
        return _regime_str("tdnolpyear", self.mit, self.regimechange)

td1coef dataclass

Bases: X13var

One-coefficient trading-day regressor with optional regime change.

Mirrors x13spec.jl:112-119.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class td1coef(X13var):
    """One-coefficient trading-day regressor with optional regime change.

    Mirrors ``x13spec.jl:112-119``.
    """

    mit: MIT | None = None
    regimechange: RegimeChange = RegimeChange.NEITHER

    def __init__(
        self,
        mit: MIT | None = None,
        regimechange: _RegimeChangeArg = None,
    ) -> None:
        resolved = _resolve_regime(mit, regimechange)
        object.__setattr__(self, "mit", mit)
        object.__setattr__(self, "regimechange", resolved)

    def __str__(self) -> str:
        return _regime_str("td1coef", self.mit, self.regimechange)

td1nolpyear dataclass

Bases: X13var

One-coefficient trading-day regressor without leap-year term.

Mirrors x13spec.jl:120-127.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class td1nolpyear(X13var):
    """One-coefficient trading-day regressor without leap-year term.

    Mirrors ``x13spec.jl:120-127``.
    """

    mit: MIT | None = None
    regimechange: RegimeChange = RegimeChange.NEITHER

    def __init__(
        self,
        mit: MIT | None = None,
        regimechange: _RegimeChangeArg = None,
    ) -> None:
        resolved = _resolve_regime(mit, regimechange)
        object.__setattr__(self, "mit", mit)
        object.__setattr__(self, "regimechange", resolved)

    def __str__(self) -> str:
        return _regime_str("td1nolpyear", self.mit, self.regimechange)

lpyear dataclass

Bases: X13var

Leap-year regressor with optional regime change.

Mirrors x13spec.jl:129-135.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class lpyear(X13var):
    """Leap-year regressor with optional regime change.

    Mirrors ``x13spec.jl:129-135``.
    """

    mit: MIT | None = None
    regimechange: RegimeChange = RegimeChange.NEITHER

    def __init__(
        self,
        mit: MIT | None = None,
        regimechange: _RegimeChangeArg = None,
    ) -> None:
        resolved = _resolve_regime(mit, regimechange)
        object.__setattr__(self, "mit", mit)
        object.__setattr__(self, "regimechange", resolved)

    def __str__(self) -> str:
        return _regime_str("lpyear", self.mit, self.regimechange)

lom dataclass

Bases: X13var

Length-of-month regressor with optional regime change.

Mirrors x13spec.jl:137-143.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class lom(X13var):
    """Length-of-month regressor with optional regime change.

    Mirrors ``x13spec.jl:137-143``.
    """

    mit: MIT | None = None
    regimechange: RegimeChange = RegimeChange.NEITHER

    def __init__(
        self,
        mit: MIT | None = None,
        regimechange: _RegimeChangeArg = None,
    ) -> None:
        resolved = _resolve_regime(mit, regimechange)
        object.__setattr__(self, "mit", mit)
        object.__setattr__(self, "regimechange", resolved)

    def __str__(self) -> str:
        return _regime_str("lom", self.mit, self.regimechange)

loq dataclass

Bases: X13var

Length-of-quarter regressor with optional regime change.

Mirrors x13spec.jl:146-152.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class loq(X13var):
    """Length-of-quarter regressor with optional regime change.

    Mirrors ``x13spec.jl:146-152``.
    """

    mit: MIT | None = None
    regimechange: RegimeChange = RegimeChange.NEITHER

    def __init__(
        self,
        mit: MIT | None = None,
        regimechange: _RegimeChangeArg = None,
    ) -> None:
        resolved = _resolve_regime(mit, regimechange)
        object.__setattr__(self, "mit", mit)
        object.__setattr__(self, "regimechange", resolved)

    def __str__(self) -> str:
        return _regime_str("loq", self.mit, self.regimechange)

seasonal dataclass

Bases: X13var

Seasonal regressor with optional regime change.

Mirrors x13spec.jl:154-161.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class seasonal(X13var):
    """Seasonal regressor with optional regime change.

    Mirrors ``x13spec.jl:154-161``.
    """

    mit: MIT | None = None
    regimechange: RegimeChange = RegimeChange.NEITHER

    def __init__(
        self,
        mit: MIT | None = None,
        regimechange: _RegimeChangeArg = None,
    ) -> None:
        resolved = _resolve_regime(mit, regimechange)
        object.__setattr__(self, "mit", mit)
        object.__setattr__(self, "regimechange", resolved)

    def __str__(self) -> str:
        return _regime_str("seasonal", self.mit, self.regimechange)

ArimaSpec dataclass

A single ARIMA(p, d, q) operator with optional period.

Mirrors x13spec.jl:216-228. p, d, q may be a non-negative :class:int (e.g. ArimaSpec(1, 1, 1) for ARIMA(1,1,1)) or a :class:tuple of ints to spell an operator with missing lags (ArimaSpec((2, 3), 0, 0) → :math:(1 - \Phi_2 B^2 - \Phi_3 B^3) z_t = a_t). period=0 (the default) means "infer the period from spec ordering" — the ARIMA builder (M2.2) uses position in the spec list to assign seasonal periods when period=0.

Mutable to mirror Julia's mutable struct: spec builders re-assign fields as they accumulate. Equality compares field-by-field.

Source code in src/tsecon/x13/_spec.py
@dataclass(slots=True)
class ArimaSpec:
    r"""A single ARIMA(p, d, q) operator with optional period.

    Mirrors ``x13spec.jl:216-228``. ``p``, ``d``, ``q`` may be a non-negative
    :class:`int` (e.g. ``ArimaSpec(1, 1, 1)`` for ARIMA(1,1,1)) or a
    :class:`tuple` of ints to spell an operator with missing lags
    (``ArimaSpec((2, 3), 0, 0)`` →
    :math:`(1 - \Phi_2 B^2 - \Phi_3 B^3) z_t = a_t`). ``period=0`` (the
    default) means "infer the period from spec ordering" — the ARIMA
    builder (M2.2) uses position in the spec list to assign seasonal
    periods when ``period=0``.

    Mutable to mirror Julia's ``mutable struct``: spec builders re-assign
    fields as they accumulate. Equality compares field-by-field.
    """

    p: _OrderValue = 0
    d: _OrderValue = 0
    q: _OrderValue = 0
    period: int | X13default = 0

    def __init__(
        self,
        p: _OrderArg = 0,
        d: _OrderArg = 0,
        q: _OrderArg = 0,
        period: int | X13default = 0,
    ) -> None:
        self.p = _coerce_order(p)
        self.d = _coerce_order(d)
        self.q = _coerce_order(q)
        if isinstance(period, bool) or not isinstance(period, (int, X13default)):
            msg = f"ArimaSpec period must be int or X13default; got {type(period).__name__}."
            raise TypeError(msg)
        self.period = period

    @classmethod
    def two_seasonal(
        cls,
        p: _OrderArg,
        d: _OrderArg,
        q: _OrderArg,
        P: _OrderArg,  # noqa: N803 — mirrors Julia's (p,d,q)(P,D,Q) signature
        D: _OrderArg,  # noqa: N803
        Q: _OrderArg,  # noqa: N803
    ) -> tuple[ArimaSpec, ArimaSpec]:
        """Build the ``(p,d,q)(P,D,Q)`` pair, mirroring Julia's 6-arg form.

        Returns a 2-tuple of :class:`ArimaSpec` — the nonseasonal then the
        seasonal — both with ``period=0`` (the ARIMA builder assigns the
        actual seasonal period from spec ordering).

        Mirrors ``x13spec.jl:227``
        (``ArimaSpec(p,d,q,P,D,Q) = (new(p,d,q,0), new(P,D,Q,0))``).
        """
        return cls(p, d, q, 0), cls(P, D, Q, 0)

two_seasonal classmethod

two_seasonal(
    p: _OrderArg,
    d: _OrderArg,
    q: _OrderArg,
    P: _OrderArg,
    D: _OrderArg,
    Q: _OrderArg,
) -> tuple[ArimaSpec, ArimaSpec]

Build the (p,d,q)(P,D,Q) pair, mirroring Julia's 6-arg form.

Returns a 2-tuple of :class:ArimaSpec — the nonseasonal then the seasonal — both with period=0 (the ARIMA builder assigns the actual seasonal period from spec ordering).

Mirrors x13spec.jl:227 (ArimaSpec(p,d,q,P,D,Q) = (new(p,d,q,0), new(P,D,Q,0))).

Source code in src/tsecon/x13/_spec.py
@classmethod
def two_seasonal(
    cls,
    p: _OrderArg,
    d: _OrderArg,
    q: _OrderArg,
    P: _OrderArg,  # noqa: N803 — mirrors Julia's (p,d,q)(P,D,Q) signature
    D: _OrderArg,  # noqa: N803
    Q: _OrderArg,  # noqa: N803
) -> tuple[ArimaSpec, ArimaSpec]:
    """Build the ``(p,d,q)(P,D,Q)`` pair, mirroring Julia's 6-arg form.

    Returns a 2-tuple of :class:`ArimaSpec` — the nonseasonal then the
    seasonal — both with ``period=0`` (the ARIMA builder assigns the
    actual seasonal period from spec ordering).

    Mirrors ``x13spec.jl:227``
    (``ArimaSpec(p,d,q,P,D,Q) = (new(p,d,q,0), new(P,D,Q,0))``).
    """
    return cls(p, d, q, 0), cls(P, D, Q, 0)

ArimaModel dataclass

A collection of :class:ArimaSpec operators with a default flag.

Mirrors x13spec.jl:232-242. The default flag marks the model as the airline-default ((0,1,1)(0,1,1)) for automdl purposes; the :func:automdl builder (M2.2) consults it to decide whether to explicitly serialize the model in the .spc file or rely on the binary's built-in default.

Use the :class:classmethod-style helpers below (mirroring Julia's six positional-only constructors) for the common ARIMA(p,d,q)[period] and ARIMA(p,d,q)(P,D,Q) call shapes; pass an explicit list of :class:ArimaSpec for fully custom multi-operator models.

Source code in src/tsecon/x13/_spec.py
@dataclass(slots=True)
class ArimaModel:
    """A collection of :class:`ArimaSpec` operators with a ``default`` flag.

    Mirrors ``x13spec.jl:232-242``. The ``default`` flag marks the model as
    the airline-default (``(0,1,1)(0,1,1)``) for ``automdl`` purposes; the
    :func:`automdl` builder (M2.2) consults it to decide whether to
    explicitly serialize the model in the ``.spc`` file or rely on the
    binary's built-in default.

    Use the :class:`classmethod`-style helpers below (mirroring Julia's
    six positional-only constructors) for the common ARIMA(p,d,q)[period]
    and ARIMA(p,d,q)(P,D,Q) call shapes; pass an explicit list of
    :class:`ArimaSpec` for fully custom multi-operator models.
    """

    specs: list[ArimaSpec]
    default: bool = False

    @classmethod
    def from_pdq(
        cls,
        p: _OrderArg,
        d: _OrderArg,
        q: _OrderArg,
        period: int = 0,
        *,
        default: bool = False,
    ) -> ArimaModel:
        """Build a single-operator ``(p, d, q)[period]`` model.

        Mirrors Julia's ``ArimaModel(p, d, q; default=...)`` and
        ``ArimaModel(p, d, q, period; default=...)``.
        """
        return cls([ArimaSpec(p, d, q, period)], default=default)

    @classmethod
    def from_pdq_seasonal(
        cls,
        p: _OrderArg,
        d: _OrderArg,
        q: _OrderArg,
        P: _OrderArg,  # noqa: N803
        D: _OrderArg,  # noqa: N803
        Q: _OrderArg,  # noqa: N803
        *,
        default: bool = False,
    ) -> ArimaModel:
        """Build a two-operator ``(p, d, q)(P, D, Q)`` model.

        Mirrors Julia's six-positional-argument
        ``ArimaModel(p, d, q, P, D, Q; default=...)``.
        """
        return cls([ArimaSpec(p, d, q, 0), ArimaSpec(P, D, Q, 0)], default=default)

    @classmethod
    def from_specs(
        cls,
        *specs: ArimaSpec,
        default: bool = False,
    ) -> ArimaModel:
        """Build from one or more pre-built :class:`ArimaSpec` instances.

        Mirrors Julia's ``ArimaModel(specs::ArimaSpec...; default=...)``.
        """
        if not specs:
            msg = "ArimaModel.from_specs requires at least one ArimaSpec."
            raise ValueError(msg)
        return cls(list(specs), default=default)

from_pdq classmethod

from_pdq(
    p: _OrderArg,
    d: _OrderArg,
    q: _OrderArg,
    period: int = 0,
    *,
    default: bool = False,
) -> ArimaModel

Build a single-operator (p, d, q)[period] model.

Mirrors Julia's ArimaModel(p, d, q; default=...) and ArimaModel(p, d, q, period; default=...).

Source code in src/tsecon/x13/_spec.py
@classmethod
def from_pdq(
    cls,
    p: _OrderArg,
    d: _OrderArg,
    q: _OrderArg,
    period: int = 0,
    *,
    default: bool = False,
) -> ArimaModel:
    """Build a single-operator ``(p, d, q)[period]`` model.

    Mirrors Julia's ``ArimaModel(p, d, q; default=...)`` and
    ``ArimaModel(p, d, q, period; default=...)``.
    """
    return cls([ArimaSpec(p, d, q, period)], default=default)

from_pdq_seasonal classmethod

from_pdq_seasonal(
    p: _OrderArg,
    d: _OrderArg,
    q: _OrderArg,
    P: _OrderArg,
    D: _OrderArg,
    Q: _OrderArg,
    *,
    default: bool = False,
) -> ArimaModel

Build a two-operator (p, d, q)(P, D, Q) model.

Mirrors Julia's six-positional-argument ArimaModel(p, d, q, P, D, Q; default=...).

Source code in src/tsecon/x13/_spec.py
@classmethod
def from_pdq_seasonal(
    cls,
    p: _OrderArg,
    d: _OrderArg,
    q: _OrderArg,
    P: _OrderArg,  # noqa: N803
    D: _OrderArg,  # noqa: N803
    Q: _OrderArg,  # noqa: N803
    *,
    default: bool = False,
) -> ArimaModel:
    """Build a two-operator ``(p, d, q)(P, D, Q)`` model.

    Mirrors Julia's six-positional-argument
    ``ArimaModel(p, d, q, P, D, Q; default=...)``.
    """
    return cls([ArimaSpec(p, d, q, 0), ArimaSpec(P, D, Q, 0)], default=default)

from_specs classmethod

from_specs(
    *specs: ArimaSpec, default: bool = False
) -> ArimaModel

Build from one or more pre-built :class:ArimaSpec instances.

Mirrors Julia's ArimaModel(specs::ArimaSpec...; default=...).

Source code in src/tsecon/x13/_spec.py
@classmethod
def from_specs(
    cls,
    *specs: ArimaSpec,
    default: bool = False,
) -> ArimaModel:
    """Build from one or more pre-built :class:`ArimaSpec` instances.

    Mirrors Julia's ``ArimaModel(specs::ArimaSpec...; default=...)``.
    """
    if not specs:
        msg = "ArimaModel.from_specs requires at least one ArimaSpec."
        raise ValueError(msg)
    return cls(list(specs), default=default)

Span dataclass

A start/end MIT pair with optional open endpoints.

Mirrors x13spec.jl:163-171. Either endpoint may be :data:None (Julia missing), meaning "use the underlying series' first or last observation". Both endpoints together define an inclusive range.

The Julia upstream also accepts fuzzy end values such as M11 or Q2 (resolved to the most recent occurrence in the series); the Python port restricts b and e to :class:MIT or :data:None in M2.2. Fuzzy endings land in a later session if a builder (e.g. x11regression) requires them.

Construct from an :class:~tsecon.mitrange.MITRange via :meth:from_range (mirrors Julia's Span(x::UnitRange{<:MIT}) = new(first(x), last(x))).

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class Span:
    """A start/end MIT pair with optional open endpoints.

    Mirrors ``x13spec.jl:163-171``. Either endpoint may be :data:`None`
    (Julia ``missing``), meaning "use the underlying series' first or last
    observation". Both endpoints together define an inclusive range.

    The Julia upstream also accepts fuzzy ``end`` values such as ``M11`` or
    ``Q2`` (resolved to the most recent occurrence in the series); the
    Python port restricts ``b`` and ``e`` to :class:`MIT` or :data:`None`
    in M2.2. Fuzzy endings land in a later session if a builder (e.g.
    ``x11regression``) requires them.

    Construct from an :class:`~tsecon.mitrange.MITRange` via
    :meth:`from_range` (mirrors Julia's
    ``Span(x::UnitRange{<:MIT}) = new(first(x), last(x))``).
    """

    b: MIT | None = None
    e: MIT | None = None

    @classmethod
    def from_range(cls, mr: MITRange) -> Span:
        """Construct from an inclusive :class:`~tsecon.mitrange.MITRange`."""
        return cls(mr.first(), mr.last())

from_range classmethod

from_range(mr: MITRange) -> Span

Construct from an inclusive :class:~tsecon.mitrange.MITRange.

Source code in src/tsecon/x13/_spec.py
@classmethod
def from_range(cls, mr: MITRange) -> Span:
    """Construct from an inclusive :class:`~tsecon.mitrange.MITRange`."""
    return cls(mr.first(), mr.last())

X13series dataclass

The series spec block — the time series + its load-time options.

Mirrors x13spec.jl:175-199 (X13series{F<:Frequency}). The Julia type-parameter F is dropped — the underlying :class:~tsecon.tseries.TSeries already carries its own :class:~tsecon.frequencies.Frequency instance.

Constructed by :func:series; never construct directly outside the spec-builder path (validation happens in :func:series, not in __init__).

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class X13series:
    """The ``series`` spec block — the time series + its load-time options.

    Mirrors ``x13spec.jl:175-199`` (``X13series{F<:Frequency}``). The Julia
    type-parameter ``F`` is dropped — the underlying :class:`~tsecon.tseries.TSeries`
    already carries its own :class:`~tsecon.frequencies.Frequency` instance.

    Constructed by :func:`series`; never construct directly outside the
    spec-builder path (validation happens in :func:`series`, not in
    ``__init__``).
    """

    appendbcst: bool | X13default
    appendfcst: bool | X13default
    comptype: str | X13default
    compwt: float | X13default
    data: TSeries
    decimals: int | X13default
    file: str | X13default
    format: str | X13default
    modelspan: MITRange | Span | X13default
    name: str | X13default
    period: int | X13default
    precision: int | X13default
    print: str | list[str] | X13default
    save: str | list[str] | X13default
    span: MITRange | Span | X13default
    start: MIT | X13default
    title: str | X13default
    type: str | X13default
    divpower: int | X13default
    missingcode: float | X13default
    missingval: float | X13default
    saveprecision: int | X13default
    trimzero: bool | str | X13default

X13arima dataclass

The arima spec block — the ARIMA(p, d, q)(P, D, Q) component.

Mirrors x13spec.jl:245-252. The model field is an :class:ArimaModel (one or more :class:ArimaSpec operators); the AR / MA initial-value vectors (ar / ma) and their fixed-flag counterparts (fixar / fixma) align position-by-position with the model's coefficient sequence. Missing initial values default to 0.1 in the binary, encoded as :data:None here.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class X13arima:
    """The ``arima`` spec block — the ARIMA(p, d, q)(P, D, Q) component.

    Mirrors ``x13spec.jl:245-252``. The ``model`` field is an
    :class:`ArimaModel` (one or more :class:`ArimaSpec` operators); the
    AR / MA initial-value vectors (``ar`` / ``ma``) and their fixed-flag
    counterparts (``fixar`` / ``fixma``) align position-by-position with
    the model's coefficient sequence. Missing initial values default to
    ``0.1`` in the binary, encoded as :data:`None` here.
    """

    model: ArimaModel
    title: str | X13default
    ar: list[_FloatOrNone] | list[float] | X13default
    ma: list[_FloatOrNone] | list[float] | X13default
    fixar: list[bool] | X13default
    fixma: list[bool] | X13default

X13automdl dataclass

The automdl spec block — automatic ARIMA model selection.

Mirrors x13spec.jl:254-272. Auto-selects the ARIMA orders given bounds on the regular/seasonal differencing and ARMA polynomial degrees. Mutually exclusive with arima.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class X13automdl:
    """The ``automdl`` spec block — automatic ARIMA model selection.

    Mirrors ``x13spec.jl:254-272``. Auto-selects the ARIMA orders given
    bounds on the regular/seasonal differencing and ARMA polynomial
    degrees. Mutually exclusive with ``arima``.
    """

    diff: list[int] | X13default
    acceptdefault: bool | X13default
    checkmu: bool | X13default
    ljungboxlimit: float | X13default
    maxorder: list[_IntOrNone] | X13default
    maxdiff: list[_IntOrNone] | X13default
    mixed: bool | X13default
    print: str | list[str] | X13default
    savelog: str | list[str] | X13default
    armalimit: float | X13default
    balanced: bool | X13default
    exactdiff: bool | str | X13default
    fcstlim: int | X13default
    hrinitial: bool | X13default
    reducecv: float | X13default
    rejectfcst: bool | X13default
    urfinal: float | X13default

X13transform dataclass

The transform spec block — pre-modeling data transformation.

Mirrors x13spec.jl:467-486. Selects between log, Box-Cox-power, or user-supplied prior-adjustment transformation modes.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class X13transform:
    """The ``transform`` spec block — pre-modeling data transformation.

    Mirrors ``x13spec.jl:467-486``. Selects between log, Box-Cox-power,
    or user-supplied prior-adjustment transformation modes.
    """

    adjust: str | X13default
    aicdiff: float | X13default
    data: TSeries | MVTSeries | X13default
    file: str | X13default
    format: str | X13default
    func: str | X13default
    mode: str | list[str] | X13default
    name: str | list[str] | X13default
    power: float | X13default
    precision: int | X13default
    print: str | list[str] | X13default
    save: str | list[str] | X13default
    savelog: str | list[str] | X13default
    start: MIT | list[MIT] | X13default
    title: str | X13default
    type: str | list[str] | X13default
    constant: float | X13default
    trimzero: bool | str | X13default

X13regression dataclass

The regression spec block — regARIMA regressors / outliers.

Mirrors x13spec.jl:383-407. Carries the predefined regressor list (variables), the user-supplied regressors (data / user), initial coefficient values (b) and fix-flags (fixb), and the AIC-comparison configuration (aictest / aicdiff).

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class X13regression:
    """The ``regression`` spec block — regARIMA regressors / outliers.

    Mirrors ``x13spec.jl:383-407``. Carries the predefined regressor list
    (``variables``), the user-supplied regressors (``data`` /  ``user``),
    initial coefficient values (``b``) and fix-flags (``fixb``), and the
    AIC-comparison configuration (``aictest`` / ``aicdiff``).
    """

    aicdiff: list[_FloatOrNone] | list[float] | X13default
    aictest: str | list[str] | X13default
    chi2test: bool | X13default
    chi2testcv: float | X13default
    data: MVTSeries | X13default
    file: str | X13default
    format: str | X13default
    print: str | list[str] | X13default
    save: str | list[str] | X13default
    savelog: str | list[str] | X13default
    pvaictest: float | X13default
    start: MIT | X13default
    testalleaster: bool | X13default
    tlimit: float | X13default
    user: str | list[str] | X13default
    usertype: str | list[str] | X13default
    variables: _VariablesField
    b: list[float] | X13default
    fixb: list[bool] | X13default
    centeruser: str | X13default
    eastermeans: bool | X13default
    noapply: str | X13default
    tcrate: float | X13default

X13forecast dataclass

The forecast spec block — out-of-sample forecast generation.

Mirrors x13spec.jl:310-318. maxlead controls how far ahead point forecasts and their variances are produced; maxback does the same for backcasts.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class X13forecast:
    """The ``forecast`` spec block — out-of-sample forecast generation.

    Mirrors ``x13spec.jl:310-318``. ``maxlead`` controls how far ahead
    point forecasts and their variances are produced; ``maxback`` does
    the same for backcasts.
    """

    exclude: int | X13default
    lognormal: bool | X13default
    maxback: int | X13default
    maxlead: int | X13default
    print: str | list[str] | X13default
    save: str | list[str] | X13default
    probability: float | X13default

X13seats dataclass

The seats spec block — model-based signal-extraction adjustment.

Mirrors x13spec.jl:409-430. Activates the SEATS module for seasonal decomposition based on the regARIMA model.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class X13seats:
    """The ``seats`` spec block — model-based signal-extraction adjustment.

    Mirrors ``x13spec.jl:409-430``. Activates the SEATS module for
    seasonal decomposition based on the regARIMA model.
    """

    appendfcst: bool | X13default
    finite: bool | X13default
    hpcycle: bool | X13default
    noadmiss: bool | X13default
    out: int | X13default
    print: str | list[str] | X13default
    save: str | list[str] | X13default
    savelog: str | list[str] | X13default
    printphtrf: bool | X13default
    qmax: int | X13default
    statseas: bool | X13default
    tabtables: list[str] | X13default
    bias: int | X13default
    epsiv: float | X13default
    epsphi: int | X13default
    hplan: int | X13default
    imean: bool | X13default
    maxit: int | X13default
    rmod: float | X13default
    xl: float | X13default

X13x11 dataclass

The x11 spec block — X-11 (enhanced) seasonal adjustment.

Mirrors x13spec.jl:489-510. The traditional Census-Bureau X-11 method, with auto-selection or per-period seasonal-MA configuration and Henderson trend-MA selection.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class X13x11:
    """The ``x11`` spec block — X-11 (enhanced) seasonal adjustment.

    Mirrors ``x13spec.jl:489-510``. The traditional Census-Bureau X-11
    method, with auto-selection or per-period seasonal-MA configuration
    and Henderson trend-MA selection.
    """

    appendbcst: bool | X13default
    appendfcst: bool | X13default
    final: str | list[str] | X13default
    mode: str | X13default
    print: str | list[str] | X13default
    save: str | list[str] | X13default
    savelog: str | list[str] | X13default
    seasonalma: str | list[str] | X13default
    sigmalim: list[_FloatOrNone] | list[float] | X13default
    title: str | list[str] | X13default
    trendma: int | X13default
    type: str | X13default
    calendarsigma: str | X13default
    centerseasonal: bool | X13default
    keepholiday: bool | X13default
    print1stpass: bool | X13default
    sfshort: bool | X13default
    sigmavec: list[str] | X13default
    trendic: float | X13default
    true7term: bool | X13default

X13check dataclass

The check spec block — regARIMA residual diagnostics.

Mirrors x13spec.jl:274-282. Carries ACF / PACF lag limits and significance thresholds used for residual-adequacy reporting.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class X13check:
    """The ``check`` spec block — regARIMA residual diagnostics.

    Mirrors ``x13spec.jl:274-282``. Carries ACF / PACF lag limits and
    significance thresholds used for residual-adequacy reporting.
    """

    maxlag: int | X13default
    qtype: str | X13default
    print: str | list[str] | X13default
    save: str | list[str] | X13default
    savelog: str | list[str] | X13default
    acflimit: float | X13default
    qlimit: float | X13default

X13estimate dataclass

The estimate spec block — regARIMA estimation controls.

Mirrors x13spec.jl:284-294. Selects exact-vs-conditional likelihood, iteration limits, and optional preloaded .mdl file with fix-policy.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class X13estimate:
    """The ``estimate`` spec block — regARIMA estimation controls.

    Mirrors ``x13spec.jl:284-294``. Selects exact-vs-conditional likelihood,
    iteration limits, and optional preloaded ``.mdl`` file with fix-policy.
    """

    exact: str | X13default
    maxiter: int | X13default
    outofsample: bool | X13default
    print: str | list[str] | X13default
    save: str | list[str] | X13default
    savelog: str | list[str] | X13default
    tol: float | X13default
    file: str | X13default
    fix: str | X13default

X13force dataclass

The force spec block — yearly-total forcing on seasonally adjusted output.

Mirrors x13spec.jl:296-308. Activates Denton or regression-based benchmarking to make the SA series' yearly totals match the target.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class X13force:
    """The ``force`` spec block — yearly-total forcing on seasonally adjusted output.

    Mirrors ``x13spec.jl:296-308``. Activates Denton or regression-based
    benchmarking to make the SA series' yearly totals match the target.
    """

    lambda_: float | X13default
    mode: str | X13default
    print: str | list[str] | X13default
    save: str | list[str] | X13default
    rho: float | X13default
    round: bool | X13default
    start: str | X13default
    target: str | X13default
    type: str | X13default
    usefcst: bool | X13default
    indforce: bool | X13default

X13history dataclass

The history spec block — revisions / forecast-error history analysis.

Mirrors x13spec.jl:321-340. Drives the truncated-series re-run pipeline used to characterise revisions and out-of-sample forecast errors at user-specified lags.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class X13history:
    """The ``history`` spec block — revisions / forecast-error history analysis.

    Mirrors ``x13spec.jl:321-340``. Drives the truncated-series re-run
    pipeline used to characterise revisions and out-of-sample forecast
    errors at user-specified lags.
    """

    endtable: MIT | X13default
    estimates: str | list[str] | X13default
    fixmdl: bool | X13default
    fixreg: bool | X13default
    fstep: int | list[int] | X13default
    print: str | list[str] | X13default
    save: str | list[str] | X13default
    savelog: str | list[str] | X13default
    sadjlags: int | list[int] | X13default
    start: MIT | X13default
    target: str | X13default
    trendlags: int | list[int] | X13default
    fixx11reg: bool | X13default
    outlier: str | X13default
    outlierwin: int | X13default
    refresh: bool | X13default
    transformfcst: bool | X13default
    x11outlier: bool | X13default

X13identify dataclass

The identify spec block — ACF / PACF plots for ARIMA identification.

Mirrors x13spec.jl:342-348. Sample ACFs and PACFs are produced for every combination of nonseasonal-difference orders (diff) and seasonal-difference orders (sdiff).

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class X13identify:
    """The ``identify`` spec block — ACF / PACF plots for ARIMA identification.

    Mirrors ``x13spec.jl:342-348``. Sample ACFs and PACFs are produced
    for every combination of nonseasonal-difference orders (``diff``) and
    seasonal-difference orders (``sdiff``).
    """

    diff: list[int] | X13default
    sdiff: list[int] | X13default
    maxlag: int | X13default
    print: str | list[str] | X13default
    save: str | list[str] | X13default

X13metadata dataclass

The metadata spec block — diagnostic-summary key/value entries.

Mirrors x13spec.jl:350-352. Stores a tuple of (key, value) pairs that X-13 emits into the .udg diagnostic-summary file.

The Julia upstream's Pair{String,String} / Vector{Pair{...}} ports as a tuple[tuple[str, str], ...] (frozen, order-preserving, pickleable) — see :func:metadata for the user-facing input shapes.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class X13metadata:
    """The ``metadata`` spec block — diagnostic-summary key/value entries.

    Mirrors ``x13spec.jl:350-352``. Stores a tuple of ``(key, value)``
    pairs that X-13 emits into the ``.udg`` diagnostic-summary file.

    The Julia upstream's ``Pair{String,String}`` / ``Vector{Pair{...}}``
    ports as a ``tuple[tuple[str, str], ...]`` (frozen, order-preserving,
    pickleable) — see :func:`metadata` for the user-facing input shapes.
    """

    entries: tuple[tuple[str, str], ...]

X13outlier dataclass

The outlier spec block — automatic outlier identification.

Mirrors x13spec.jl:354-365. Triggers the iterative add-detection loop for additive outliers, level shifts, and temporary changes.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class X13outlier:
    """The ``outlier`` spec block — automatic outlier identification.

    Mirrors ``x13spec.jl:354-365``. Triggers the iterative add-detection
    loop for additive outliers, level shifts, and temporary changes.
    """

    critical: float | list[float | None] | list[float] | X13default
    lsrun: int | X13default
    method: str | X13default
    print: str | list[str] | X13default
    save: str | list[str] | X13default
    savelog: str | list[str] | X13default
    span: MITRange | Span | X13default
    types: str | list[str] | X13default
    almost: float | X13default
    tcrate: float | X13default

X13pickmdl dataclass

The pickmdl spec block — X-11-ARIMA model selection from candidates.

Mirrors x13spec.jl:367-380. Picks the ARIMA part from either a list of candidate :class:ArimaModel instances or a file containing such a list.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class X13pickmdl:
    """The ``pickmdl`` spec block — X-11-ARIMA model selection from candidates.

    Mirrors ``x13spec.jl:367-380``. Picks the ARIMA part from either a
    list of candidate :class:`ArimaModel` instances or a file containing
    such a list.
    """

    bcstlim: int | X13default
    fcstlim: int | X13default
    models: list[ArimaModel] | X13default
    identify: str | X13default
    method: str | X13default
    mode: str | X13default
    outofsample: bool | X13default
    overdiff: float | X13default
    print: str | list[str] | X13default
    savelog: str | list[str] | X13default
    qlim: int | X13default
    file: str | X13default

X13slidingspans dataclass

The slidingspans spec block — stability analysis over moving spans.

Mirrors x13spec.jl:432-448. Compares seasonal-adjustment output across overlapping subspans of the series for revisions stability.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class X13slidingspans:
    """The ``slidingspans`` spec block — stability analysis over moving spans.

    Mirrors ``x13spec.jl:432-448``. Compares seasonal-adjustment output
    across overlapping subspans of the series for revisions stability.
    """

    cutchng: float | X13default
    cutseas: float | X13default
    cuttd: float | X13default
    fixmdl: bool | str | X13default
    fixreg: list[str] | X13default
    length: int | X13default
    numspans: int | X13default
    outlier: str | X13default
    print: str | list[str] | X13default
    save: str | list[str] | X13default
    savelog: str | list[str] | X13default
    start: MIT | X13default
    additivesa: str | X13default
    fixx11reg: bool | X13default
    x11outlier: bool | X13default

X13spectrum dataclass

The spectrum spec block — frequency-domain seasonality diagnostics.

Mirrors x13spec.jl:450-465. Computes AR-spectrum or periodogram estimates plus the QS statistic for both monthly and quarterly series.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class X13spectrum:
    """The ``spectrum`` spec block — frequency-domain seasonality diagnostics.

    Mirrors ``x13spec.jl:450-465``. Computes AR-spectrum or periodogram
    estimates plus the QS statistic for both monthly and quarterly series.
    """

    logqs: bool | X13default
    print: str | list[str] | X13default
    save: str | list[str] | X13default
    savelog: str | list[str] | X13default
    qcheck: bool | X13default
    start: MIT | X13default
    tukey120: bool | X13default
    decibel: bool | X13default
    difference: bool | str | X13default
    maxar: int | X13default
    peakwidth: int | X13default
    series: str | X13default
    siglevel: int | X13default
    type: str | X13default

X13x11regression dataclass

The x11regression spec block — calendar / outlier regression on irregulars.

Mirrors x13spec.jl:512-547. The X-11 sibling of the regARIMA :class:X13regression block — applies calendar / trading-day regressions to the X-11 irregular component.

Source code in src/tsecon/x13/_spec.py
@dataclass(frozen=True, slots=True)
class X13x11regression:
    """The ``x11regression`` spec block — calendar / outlier regression on irregulars.

    Mirrors ``x13spec.jl:512-547``. The X-11 sibling of the regARIMA
    :class:`X13regression` block — applies calendar / trading-day
    regressions to the X-11 irregular component.
    """

    aicdiff: float | X13default
    aictest: str | list[str] | X13default
    critical: float | X13default
    data: MVTSeries | X13default
    file: str | X13default
    format: str | X13default
    outliermethod: str | X13default
    outlierspan: MITRange | Span | X13default
    print: str | list[str] | X13default
    save: str | list[str] | X13default
    savelog: str | list[str] | X13default
    prior: bool | X13default
    sigma: float | X13default
    span: MITRange | Span | X13default
    start: MIT | X13default
    tdprior: list[float] | X13default
    user: str | list[str] | X13default
    usertype: str | list[str] | X13default
    variables: _VariablesField
    almost: float | X13default
    b: list[float] | X13default
    fixb: list[bool] | X13default
    centeruser: str | X13default
    eastermeans: bool | X13default
    forcecal: bool | X13default
    noapply: list[str] | X13default
    reweight: bool | X13default
    umdata: MVTSeries | X13default
    umfile: str | X13default
    umformat: str | X13default
    umname: list[str] | str | X13default
    umprecision: int | X13default
    umstart: MIT | X13default
    umtrimzero: bool | str | X13default

X13spec dataclass

The X-13 spec aggregator — one series plus up to 18 sub-specs.

Mirrors x13spec.jl:552-575 (mutable struct X13spec{F<:Frequency}). The Julia type-parameter F is dropped — the contained :class:X13series already carries its :class:~tsecon.frequencies.Frequency via its data field.

Construct via :func:newspec (passing either a :class:~tsecon.tseries.TSeries or an already-built :class:X13series); the bang-suffixed Julia setters (arima! / outlier! / …) port as plain attribute assignment because this dataclass is intentionally mutable and slotted — the surface mirrors Julia's spec.outlier = outlier(...) ergonomics. Re-assignment is the only mutation the M2 surface supports; the contained sub-spec dataclasses themselves remain frozen.

The folder and string fields are populated by the M2.5 binary runner: folder holds the temp dir the run created, string holds the rendered .spc text (mirrors Julia's spec.string side-effect inside x13write).

Source code in src/tsecon/x13/_spec.py
@dataclass(slots=True)
class X13spec:
    """The X-13 spec aggregator — one ``series`` plus up to 18 sub-specs.

    Mirrors ``x13spec.jl:552-575`` (``mutable struct X13spec{F<:Frequency}``).
    The Julia type-parameter ``F`` is dropped — the contained
    :class:`X13series` already carries its :class:`~tsecon.frequencies.Frequency`
    via its ``data`` field.

    Construct via :func:`newspec` (passing either a :class:`~tsecon.tseries.TSeries`
    or an already-built :class:`X13series`); the bang-suffixed Julia
    setters (``arima!`` / ``outlier!`` / …) port as plain attribute
    assignment because this dataclass is intentionally **mutable** and
    **slotted** — the surface mirrors Julia's ``spec.outlier = outlier(...)``
    ergonomics. Re-assignment is the only mutation the M2 surface
    supports; the contained sub-spec dataclasses themselves remain
    frozen.

    The ``folder`` and ``string`` fields are populated by the M2.5 binary
    runner: ``folder`` holds the temp dir the run created, ``string``
    holds the rendered ``.spc`` text (mirrors Julia's ``spec.string``
    side-effect inside ``x13write``).
    """

    series: X13series | X13default
    arima: X13arima | X13default
    estimate: X13estimate | X13default
    transform: X13transform | X13default
    regression: X13regression | X13default
    automdl: X13automdl | X13default
    x11: X13x11 | X13default
    x11regression: X13x11regression | X13default
    check: X13check | X13default
    forecast: X13forecast | X13default
    force: X13force | X13default
    pickmdl: X13pickmdl | X13default
    history: X13history | X13default
    metadata: X13metadata | X13default
    identify: X13identify | X13default
    outlier: X13outlier | X13default
    seats: X13seats | X13default
    slidingspans: X13slidingspans | X13default
    spectrum: X13spectrum | X13default
    folder: str | X13default
    string: str | X13default

series

series(
    t: TSeries,
    *,
    appendbcst: bool | X13default = _X13DEFAULT,
    appendfcst: bool | X13default = _X13DEFAULT,
    comptype: str | X13default = _X13DEFAULT,
    compwt: float | X13default = _X13DEFAULT,
    decimals: int | X13default = _X13DEFAULT,
    file: str | X13default = _X13DEFAULT,
    format: str | X13default = _X13DEFAULT,
    modelspan: MITRange | Span | X13default = _X13DEFAULT,
    name: str | X13default = _X13DEFAULT,
    period: int | X13default = _X13DEFAULT,
    precision: int | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    span: MITRange | Span | X13default = _X13DEFAULT,
    start: MIT | X13default = _X13DEFAULT,
    title: str | X13default = _X13DEFAULT,
    type: str | X13default = _X13DEFAULT,
    divpower: int | X13default = _X13DEFAULT,
    missingcode: float | X13default = _X13DEFAULT,
    missingval: float | X13default = _X13DEFAULT,
    saveprecision: int | X13default = _X13DEFAULT,
    trimzero: bool | str | X13default = _X13DEFAULT,
) -> X13series

Build the series spec — required for every X-13 run.

Mirrors x13spec.jl:732-822. The t argument is the input :class:~tsecon.tseries.TSeries; all other arguments are keyword-only and mirror the Julia spec's named parameters one-for-one. Defaults are the :data:_X13DEFAULT sentinel — fields holding the sentinel are omitted by the M2.4 .spc writer, letting X-13 apply its built-in default.

Validation performed:

  • name and title truncated (with a :class:UserWarning) to 64 and 79 characters respectively, mirroring Julia.
  • period is auto-set to ppy(t) for non-Monthly / non-Yearly frequencies (Quarterly defaults to 4, etc.).
  • span (whether :class:~tsecon.mitrange.MITRange or :class:Span) must be contained within t.range.
  • :class:Span span endpoints must be :class:MIT or :data:None — fuzzy endings (Julia's M11 / Q2) are rejected here per upstream.
  • divpower must be in [-9, 9].
  • If t.values contains NaN, missingcode must be set; the output's data field has NaN replaced with missingcode in a fresh copy (input t is not mutated).
  • print="all" / save="all" expand to the upstream-defined "everything" lists.

Returns an :class:X13series carrying the (possibly cropped / NaN-replaced) data and the validated field values.

Source code in src/tsecon/x13/_spec.py
def series(
    t: TSeries,
    *,
    appendbcst: bool | X13default = _X13DEFAULT,
    appendfcst: bool | X13default = _X13DEFAULT,
    comptype: str | X13default = _X13DEFAULT,
    compwt: float | X13default = _X13DEFAULT,
    decimals: int | X13default = _X13DEFAULT,
    file: str | X13default = _X13DEFAULT,
    format: str | X13default = _X13DEFAULT,
    modelspan: MITRange | Span | X13default = _X13DEFAULT,
    name: str | X13default = _X13DEFAULT,
    period: int | X13default = _X13DEFAULT,
    precision: int | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    span: MITRange | Span | X13default = _X13DEFAULT,
    start: MIT | X13default = _X13DEFAULT,
    title: str | X13default = _X13DEFAULT,
    type: str | X13default = _X13DEFAULT,
    divpower: int | X13default = _X13DEFAULT,
    missingcode: float | X13default = _X13DEFAULT,
    missingval: float | X13default = _X13DEFAULT,
    saveprecision: int | X13default = _X13DEFAULT,
    trimzero: bool | str | X13default = _X13DEFAULT,
) -> X13series:
    """Build the ``series`` spec — required for every X-13 run.

    Mirrors ``x13spec.jl:732-822``. The ``t`` argument is the input
    :class:`~tsecon.tseries.TSeries`; all other arguments are keyword-only
    and mirror the Julia spec's named parameters one-for-one. Defaults are
    the :data:`_X13DEFAULT` sentinel — fields holding the sentinel are
    omitted by the M2.4 ``.spc`` writer, letting X-13 apply its built-in
    default.

    Validation performed:

    * ``name`` and ``title`` truncated (with a :class:`UserWarning`) to 64
      and 79 characters respectively, mirroring Julia.
    * ``period`` is auto-set to ``ppy(t)`` for non-Monthly / non-Yearly
      frequencies (Quarterly defaults to 4, etc.).
    * ``span`` (whether :class:`~tsecon.mitrange.MITRange` or
      :class:`Span`) must be contained within ``t.range``.
    * :class:`Span` ``span`` endpoints must be :class:`MIT` or
      :data:`None` — fuzzy endings (Julia's ``M11`` / ``Q2``) are
      rejected here per upstream.
    * ``divpower`` must be in ``[-9, 9]``.
    * If ``t.values`` contains ``NaN``, ``missingcode`` must be set; the
      output's ``data`` field has NaN replaced with ``missingcode`` in a
      fresh copy (input ``t`` is not mutated).
    * ``print="all"`` / ``save="all"`` expand to the upstream-defined
      "everything" lists.

    Returns an :class:`X13series` carrying the (possibly cropped /
    NaN-replaced) data and the validated field values.
    """
    if not isinstance(t, TSeries):
        # Runtime guard: the annotation says TSeries so mypy marks this path
        # unreachable. Callers that bypass the annotation (``Any``-typed glue
        # code, dynamic dispatch) still hit a sharp error rather than failing
        # mid-derivation on ``t.range``.
        msg = (  # type: ignore[unreachable]
            f"series() requires a TSeries; got {t.__class__.__name__}."
        )
        raise TypeError(msg)

    data = t.copy()
    if not isinstance(start, X13default):
        if start < data.range.first() or start > data.range.last():
            msg = f"series() start={start!r} must be within the series range {data.range!r}."
            raise ValueError(msg)
        # Crop to start..end (inclusive).
        start_idx = start - data.range.first()
        data = TSeries(start, data.values[start_idx:].copy())
        start_value: MIT | X13default = start
    else:
        start_value = data.range.first()

    if isinstance(name, str) and len(name) > 64:
        warnings.warn(
            f"Series name truncated to 64 characters. Full name: {name}",
            UserWarning,
            stacklevel=2,
        )
        name = name[:64]

    if isinstance(title, str) and len(title) > 79:
        warnings.warn(
            f"Series title truncated to 79 characters. Full title: {title}",
            UserWarning,
            stacklevel=2,
        )
        title = title[:79]

    if not isinstance(t.frequency, Monthly) and not isinstance(t.frequency, Yearly):
        period = ppy(t.frequency)

    _check_span_against(span, t, arg_name="span")
    _check_span_against(modelspan, t, arg_name="modelspan")

    if not isinstance(divpower, X13default) and (divpower < -9 or divpower > 9):
        msg = f"divpower values must be between -9 and 9 (inclusive). Received: {divpower}."
        raise ValueError(msg)

    if np.isnan(data.values).any():
        if isinstance(missingcode, X13default):
            msg = (
                "The provided tseries has NaN values but no `missingcode` "
                "was specified. Please specify a missingcode for your "
                "Series argument. I.e. missingcode = -99999.0."
            )
            raise ValueError(msg)
        data = data.copy()
        nan_mask = np.isnan(data.values)
        data.values[nan_mask] = missingcode

    print = _expand_all(print, _SERIES_PRINT_ALL)
    save = _expand_all(save, _SERIES_SAVE_ALL)

    return X13series(
        appendbcst=appendbcst,
        appendfcst=appendfcst,
        comptype=comptype,
        compwt=compwt,
        data=data,
        decimals=decimals,
        file=file,
        format=format,
        modelspan=modelspan,
        name=name,
        period=period,
        precision=precision,
        print=print,
        save=save,
        span=span,
        start=start_value,
        title=title,
        type=type,
        divpower=divpower,
        missingcode=missingcode,
        missingval=missingval,
        saveprecision=saveprecision,
        trimzero=trimzero,
    )

arima

arima(
    model: ArimaModel | ArimaSpec | tuple[ArimaSpec, ...],
    *,
    title: str | X13default = _X13DEFAULT,
    ar: _ArArg = _X13DEFAULT,
    ma: _ArArg = _X13DEFAULT,
    fixar: list[bool] | X13default = _X13DEFAULT,
    fixma: list[bool] | X13default = _X13DEFAULT,
) -> X13arima

Build the arima spec — the ARIMA part of the regARIMA model.

Mirrors x13spec.jl:873-911. Accepts any of:

  • an :class:ArimaModel (the primary Julia overload),
  • a single :class:ArimaSpec (wrapped via :meth:ArimaModel.from_specs),
  • a tuple of :class:ArimaSpec (wrapped via :meth:ArimaModel.from_specs).

The two-positional-tuple form mirrors :meth:ArimaSpec.two_seasonal's return shape, so arima(ArimaSpec.two_seasonal(1,1,1,0,1,1)) ports Julia's arima(ArimaSpec(1,1,1,0,1,1)...) idiom.

Validation:

  • fixar length must match ar length (if both set).
  • fixma length must match ma length (if both set).
  • title truncated to 79 chars with a :class:UserWarning.
Source code in src/tsecon/x13/_spec.py
def arima(
    model: ArimaModel | ArimaSpec | tuple[ArimaSpec, ...],
    *,
    title: str | X13default = _X13DEFAULT,
    ar: _ArArg = _X13DEFAULT,
    ma: _ArArg = _X13DEFAULT,
    fixar: list[bool] | X13default = _X13DEFAULT,
    fixma: list[bool] | X13default = _X13DEFAULT,
) -> X13arima:
    """Build the ``arima`` spec — the ARIMA part of the regARIMA model.

    Mirrors ``x13spec.jl:873-911``. Accepts any of:

    * an :class:`ArimaModel` (the primary Julia overload),
    * a single :class:`ArimaSpec` (wrapped via :meth:`ArimaModel.from_specs`),
    * a tuple of :class:`ArimaSpec` (wrapped via :meth:`ArimaModel.from_specs`).

    The two-positional-tuple form mirrors :meth:`ArimaSpec.two_seasonal`'s
    return shape, so ``arima(ArimaSpec.two_seasonal(1,1,1,0,1,1))`` ports
    Julia's ``arima(ArimaSpec(1,1,1,0,1,1)...)`` idiom.

    Validation:

    * ``fixar`` length must match ``ar`` length (if both set).
    * ``fixma`` length must match ``ma`` length (if both set).
    * ``title`` truncated to 79 chars with a :class:`UserWarning`.
    """
    if isinstance(model, ArimaSpec):
        wrapped: ArimaModel = ArimaModel.from_specs(model)
    elif isinstance(model, tuple):
        wrapped = ArimaModel.from_specs(*model)
    elif isinstance(model, ArimaModel):
        wrapped = model
    else:
        # Type annotation enumerates every branch above; this catch-all
        # guards Any-typed glue code from M2.4 (X13spec.arima = arima(...)).
        msg = (  # type: ignore[unreachable]
            f"arima() model must be ArimaModel, ArimaSpec, or tuple of "
            f"ArimaSpec; got {model.__class__.__name__}."
        )
        raise TypeError(msg)

    if (
        not isinstance(fixar, X13default)
        and not isinstance(ar, X13default)
        and len(fixar) != len(ar)
    ):
        msg = f"fixar must have the same length as ar. Provided ar={ar}, fixar={fixar}."
        raise ValueError(msg)
    if (
        not isinstance(fixma, X13default)
        and not isinstance(ma, X13default)
        and len(fixma) != len(ma)
    ):
        msg = f"fixma must have the same length as ma. Provided ma={ma}, fixma={fixma}."
        raise ValueError(msg)

    if isinstance(title, str) and len(title) > 79:
        warnings.warn(
            f"Arima title truncated to 79 characters. Full title: {title}",
            UserWarning,
            stacklevel=2,
        )
        title = title[:79]

    return X13arima(
        model=wrapped,
        title=title,
        ar=ar,
        ma=ma,
        fixar=fixar,
        fixma=fixma,
    )

automdl

automdl(
    *,
    diff: list[int] | X13default = _X13DEFAULT,
    acceptdefault: bool | X13default = _X13DEFAULT,
    checkmu: bool | X13default = _X13DEFAULT,
    ljungboxlimit: float | X13default = _X13DEFAULT,
    maxorder: list[_IntOrNone] | X13default = _X13DEFAULT,
    maxdiff: list[_IntOrNone] | X13default = _X13DEFAULT,
    mixed: bool | X13default = _X13DEFAULT,
    print: str | list[str] | X13default | None = None,
    savelog: str | list[str] | X13default | None = None,
    armalimit: float | X13default = _X13DEFAULT,
    balanced: bool | X13default = _X13DEFAULT,
    exactdiff: bool | str | X13default = _X13DEFAULT,
    fcstlim: int | X13default = _X13DEFAULT,
    hrinitial: bool | X13default = _X13DEFAULT,
    reducecv: float | X13default = _X13DEFAULT,
    rejectfcst: bool | X13default = _X13DEFAULT,
    urfinal: float | X13default = _X13DEFAULT,
) -> X13automdl

Build the automdl spec — automatic ARIMA model selection.

Mirrors x13spec.jl:1034-1141. The print and savelog arguments default (in upstream Julia) to specific multi-element lists, not :data:_X13DEFAULT. In Python the None sentinel triggers the same defaults (a non-:data:X13default default would otherwise be serialized as the user's choice, masking that the binary's default differs).

Validation:

  • diff must be length 2; values ∈ {0, 1, 2} (regular) and {0, 1} (seasonal).
  • maxdiff must be length 2; values ∈ {1, 2} (regular) and {1} (seasonal). :data:None slots accepted.
  • maxorder must be length 2; values ∈ {1, 2, 3, 4} (regular) and {1, 2} (seasonal). :data:None slots accepted.
  • diff is ignored if both diff and maxdiff are set; a :class:UserWarning flags the redundancy (upstream Julia parity).
Source code in src/tsecon/x13/_spec.py
def automdl(  # noqa: PLR0912
    *,
    diff: list[int] | X13default = _X13DEFAULT,
    acceptdefault: bool | X13default = _X13DEFAULT,
    checkmu: bool | X13default = _X13DEFAULT,
    ljungboxlimit: float | X13default = _X13DEFAULT,
    maxorder: list[_IntOrNone] | X13default = _X13DEFAULT,
    maxdiff: list[_IntOrNone] | X13default = _X13DEFAULT,
    mixed: bool | X13default = _X13DEFAULT,
    print: str | list[str] | X13default | None = None,
    savelog: str | list[str] | X13default | None = None,
    armalimit: float | X13default = _X13DEFAULT,
    balanced: bool | X13default = _X13DEFAULT,
    exactdiff: bool | str | X13default = _X13DEFAULT,
    fcstlim: int | X13default = _X13DEFAULT,
    hrinitial: bool | X13default = _X13DEFAULT,
    reducecv: float | X13default = _X13DEFAULT,
    rejectfcst: bool | X13default = _X13DEFAULT,
    urfinal: float | X13default = _X13DEFAULT,
) -> X13automdl:
    """Build the ``automdl`` spec — automatic ARIMA model selection.

    Mirrors ``x13spec.jl:1034-1141``. The ``print`` and ``savelog``
    arguments default (in upstream Julia) to specific multi-element lists,
    not :data:`_X13DEFAULT`. In Python the ``None`` sentinel triggers the
    same defaults (a non-:data:`X13default` default would otherwise be
    serialized as the *user's* choice, masking that the binary's default
    differs).

    Validation:

    * ``diff`` must be length 2; values ``∈ {0, 1, 2}`` (regular) and
      ``{0, 1}`` (seasonal).
    * ``maxdiff`` must be length 2; values ``∈ {1, 2}`` (regular) and
      ``{1}`` (seasonal). :data:`None` slots accepted.
    * ``maxorder`` must be length 2; values ``∈ {1, 2, 3, 4}`` (regular)
      and ``{1, 2}`` (seasonal). :data:`None` slots accepted.
    * ``diff`` is ignored if both ``diff`` and ``maxdiff`` are set; a
      :class:`UserWarning` flags the redundancy (upstream Julia parity).
    """
    if print is None:
        print = list(_AUTOMDL_DEFAULT_PRINT)
    if savelog is None:
        savelog = "alldiagnostics"

    if not isinstance(diff, X13default):
        if len(diff) != 2:
            msg = "The diff argument of the automdl spec must contain exactly two values."
            raise ValueError(msg)
        if diff[0] not in (0, 1, 2):
            msg = (
                f"Acceptable values for the regular differencing orders of "
                f"the automdl spec are 0, 1, and 2. Received: {diff[0]}."
            )
            raise ValueError(msg)
        if diff[1] not in (0, 1):
            msg = (
                f"Acceptable values for the seasonal differencing orders of "
                f"the automdl spec are 0 and 1. Received: {diff[1]}."
            )
            raise ValueError(msg)
        if not isinstance(maxdiff, X13default):
            warnings.warn(
                "The diff argument of the automdl spec will be ignored "
                "because a maxdiff argument is specified.",
                UserWarning,
                stacklevel=2,
            )

    if not isinstance(maxdiff, X13default):
        if len(maxdiff) != 2:
            msg = "The maxdiff argument of the automdl spec must contain exactly two values."
            raise ValueError(msg)
        if maxdiff[0] is not None and maxdiff[0] not in (1, 2):
            msg = (
                f"Acceptable values for the regular maximum differencing "
                f"orders of the automdl spec are 1 and 2. "
                f"Received: {maxdiff[0]}."
            )
            raise ValueError(msg)
        if maxdiff[1] is not None and maxdiff[1] not in (1,):
            msg = (
                f"The only acceptable value for the seasonal maximum "
                f"differencing order of the automdl spec is 1. "
                f"Received: {maxdiff[1]}."
            )
            raise ValueError(msg)

    if not isinstance(maxorder, X13default):
        if len(maxorder) != 2:
            msg = "The maxorder argument of the automdl spec must contain exactly two values."
            raise ValueError(msg)
        if maxorder[0] is not None and maxorder[0] not in (1, 2, 3, 4):
            msg = (
                f"The maximum order for the regular ARMA model must be "
                f"greater than zero and can be at most 4. "
                f"Received: {maxorder[0]}."
            )
            raise ValueError(msg)
        if maxorder[1] is not None and maxorder[1] not in (1, 2):
            msg = (
                f"The maximum order for the seasonal ARMA model can be "
                f"either 1 or 2. Received: {maxorder[1]}."
            )
            raise ValueError(msg)

    return X13automdl(
        diff=diff,
        acceptdefault=acceptdefault,
        checkmu=checkmu,
        ljungboxlimit=ljungboxlimit,
        maxorder=maxorder,
        maxdiff=maxdiff,
        mixed=mixed,
        print=print,
        savelog=savelog,
        armalimit=armalimit,
        balanced=balanced,
        exactdiff=exactdiff,
        fcstlim=fcstlim,
        hrinitial=hrinitial,
        reducecv=reducecv,
        rejectfcst=rejectfcst,
        urfinal=urfinal,
    )

transform

transform(
    *,
    adjust: str | X13default = _X13DEFAULT,
    aicdiff: float | X13default = _X13DEFAULT,
    data: TSeries | MVTSeries | X13default = _X13DEFAULT,
    file: str | X13default = _X13DEFAULT,
    format: str | X13default = _X13DEFAULT,
    func: str | X13default = _X13DEFAULT,
    mode: str | list[str] | X13default = _X13DEFAULT,
    power: float | X13default = _X13DEFAULT,
    precision: int | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    savelog: str | list[str] | X13default | None = None,
    title: str | X13default = _X13DEFAULT,
    type: str | list[str] | X13default = _X13DEFAULT,
    constant: float | X13default = _X13DEFAULT,
    trimzero: bool | str | X13default = _X13DEFAULT,
) -> X13transform

Build the transform spec — pre-modeling transformation selector.

Mirrors x13spec.jl:2928-3010. Derives start and name from data when set (mirrors the Julia upstream's derivation block).

Validation:

  • power and func are mutually exclusive.
  • adjust="lpyear" is only valid with a log-transform (power=0.0 or func="log").
  • mode is at most two values; "diff" is incompatible with "ratio" / "percent" in the same list.
  • title truncated to 79 chars with a :class:UserWarning.
  • type requires data set; type list length must match the number of provided series.
Source code in src/tsecon/x13/_spec.py
def transform(  # noqa: PLR0912
    *,
    adjust: str | X13default = _X13DEFAULT,
    aicdiff: float | X13default = _X13DEFAULT,
    data: TSeries | MVTSeries | X13default = _X13DEFAULT,
    file: str | X13default = _X13DEFAULT,
    format: str | X13default = _X13DEFAULT,
    func: str | X13default = _X13DEFAULT,
    mode: str | list[str] | X13default = _X13DEFAULT,
    power: float | X13default = _X13DEFAULT,
    precision: int | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    savelog: str | list[str] | X13default | None = None,
    title: str | X13default = _X13DEFAULT,
    type: str | list[str] | X13default = _X13DEFAULT,
    constant: float | X13default = _X13DEFAULT,
    trimzero: bool | str | X13default = _X13DEFAULT,
) -> X13transform:
    """Build the ``transform`` spec — pre-modeling transformation selector.

    Mirrors ``x13spec.jl:2928-3010``. Derives ``start`` and ``name`` from
    ``data`` when set (mirrors the Julia upstream's derivation block).

    Validation:

    * ``power`` and ``func`` are mutually exclusive.
    * ``adjust="lpyear"`` is only valid with a log-transform
      (``power=0.0`` or ``func="log"``).
    * ``mode`` is at most two values; ``"diff"`` is incompatible with
      ``"ratio"`` / ``"percent"`` in the same list.
    * ``title`` truncated to 79 chars with a :class:`UserWarning`.
    * ``type`` requires ``data`` set; ``type`` list length must match
      the number of provided series.
    """
    if savelog is None:
        savelog = "autotransform"

    start: MIT | list[MIT] | X13default = _X13DEFAULT
    name: str | list[str] | X13default = _X13DEFAULT
    if not isinstance(data, X13default):
        start = data.range.first()
        if isinstance(data, MVTSeries):
            names = list(data.column_names)
            name = names[0] if len(names) == 1 else names

    if not isinstance(func, X13default) and not isinstance(power, X13default):
        msg = "Either power or func can be specified, but not both."
        raise ValueError(msg)

    if not isinstance(adjust, X13default) and adjust == "lpyear":
        if not isinstance(power, X13default) and power != 0.0:
            msg = "adjust='lpyear' is only allowed when a log-transform (power=0.0) is specified."
            raise ValueError(msg)
        if not isinstance(func, X13default) and func != "log":
            msg = "adjust='lpyear' is only allowed when a log-transform (func='log') is specified."
            raise ValueError(msg)

    if isinstance(mode, list):
        if len(mode) > 2:
            msg = (
                f"Only up to two values can be included in the mode "
                f"argument. Received: {len(mode)}."
            )
            raise ValueError(msg)
        if "diff" in mode and ("ratio" in mode or "percent" in mode):
            msg = (
                f"The 'diff' mode is not compatible with the 'ratio' or "
                f"'percent' modes. Received: {mode}."
            )
            raise ValueError(msg)

    if isinstance(title, str) and len(title) > 79:
        warnings.warn(
            f"Transform title truncated to 79 characters. Full title: {title}",
            UserWarning,
            stacklevel=2,
        )
        title = title[:79]

    if not isinstance(type, X13default):
        if isinstance(data, X13default):
            msg = (
                "A user-defined prior-adjustment type is specified, but no data has been provided."
            )
            raise ValueError(msg)
        if isinstance(data, TSeries) and isinstance(type, list) and len(type) > 1:
            msg = (
                f"The number of user-defined prior adjustment types "
                f"provided ({len(type)}) must match the number of data "
                f"series provided (1)."
            )
            raise ValueError(msg)
        if isinstance(data, MVTSeries):
            ncols = data.shape[1]
            if isinstance(type, list) and len(type) != ncols:
                msg = (
                    f"The number of user-defined prior adjustment types "
                    f"provided ({len(type)}) must match the number of data "
                    f"series provided ({ncols})."
                )
                raise ValueError(msg)
            if isinstance(type, str) and ncols != 1:
                msg = (
                    f"The number of user-defined prior adjustment types "
                    f"provided (1) must match the number of data series "
                    f"provided ({ncols})."
                )
                raise ValueError(msg)

    print = _expand_all(print, _TRANSFORM_PRINT_ALL)
    save = _expand_all(save, _TRANSFORM_SAVE_ALL)

    return X13transform(
        adjust=adjust,
        aicdiff=aicdiff,
        data=data,
        file=file,
        format=format,
        func=func,
        mode=mode,
        name=name,
        power=power,
        precision=precision,
        print=print,
        save=save,
        savelog=savelog,
        start=start,
        title=title,
        type=type,
        constant=constant,
        trimzero=trimzero,
    )

regression

regression(
    *,
    aicdiff: list[_FloatOrNone]
    | list[float]
    | X13default = _X13DEFAULT,
    aictest: str | list[str] | X13default = _X13DEFAULT,
    chi2test: bool | X13default = _X13DEFAULT,
    chi2testcv: float | X13default = _X13DEFAULT,
    data: MVTSeries | X13default = _X13DEFAULT,
    file: str | X13default = _X13DEFAULT,
    format: str | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    savelog: str | list[str] | X13default | None = None,
    pvaictest: float | X13default = _X13DEFAULT,
    testalleaster: bool | X13default = _X13DEFAULT,
    tlimit: float | X13default = _X13DEFAULT,
    usertype: str | list[str] | X13default = _X13DEFAULT,
    variables: _VariablesField = _X13DEFAULT,
    b: list[float] | X13default = _X13DEFAULT,
    fixb: list[bool] | X13default = _X13DEFAULT,
    centeruser: str | X13default = _X13DEFAULT,
    eastermeans: bool | X13default = _X13DEFAULT,
    noapply: str | X13default = _X13DEFAULT,
    tcrate: float | X13default = _X13DEFAULT,
) -> X13regression

Build the regression spec — regressors / outliers for regARIMA.

Mirrors x13spec.jl:2219-2354. The start and user fields are derived from data (mirrors the Julia upstream's derivation; no user-facing start= / user= kwargs are accepted because Julia overrides them).

Validation:

  • aicdiff and pvaictest are mutually exclusive.
  • usertype (when set) must be one of the documented X-13 effect types; vector form's length must match the number of user series.
  • aictest (when set) must be in the documented set of testable effects.
  • Per-variable n bounds checked for tdstock / easter / labor / thank / sceaster / easterstock.
  • Overlapping aos / lss ranges emit a :class:UserWarning.
Source code in src/tsecon/x13/_spec.py
def regression(  # noqa: PLR0912
    *,
    aicdiff: list[_FloatOrNone] | list[float] | X13default = _X13DEFAULT,
    aictest: str | list[str] | X13default = _X13DEFAULT,
    chi2test: bool | X13default = _X13DEFAULT,
    chi2testcv: float | X13default = _X13DEFAULT,
    data: MVTSeries | X13default = _X13DEFAULT,
    file: str | X13default = _X13DEFAULT,
    format: str | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    savelog: str | list[str] | X13default | None = None,
    pvaictest: float | X13default = _X13DEFAULT,
    testalleaster: bool | X13default = _X13DEFAULT,
    tlimit: float | X13default = _X13DEFAULT,
    usertype: str | list[str] | X13default = _X13DEFAULT,
    variables: _VariablesField = _X13DEFAULT,
    b: list[float] | X13default = _X13DEFAULT,
    fixb: list[bool] | X13default = _X13DEFAULT,
    centeruser: str | X13default = _X13DEFAULT,
    eastermeans: bool | X13default = _X13DEFAULT,
    noapply: str | X13default = _X13DEFAULT,
    tcrate: float | X13default = _X13DEFAULT,
) -> X13regression:
    """Build the ``regression`` spec — regressors / outliers for regARIMA.

    Mirrors ``x13spec.jl:2219-2354``. The ``start`` and ``user`` fields
    are derived from ``data`` (mirrors the Julia upstream's derivation;
    no user-facing ``start=`` / ``user=`` kwargs are accepted because
    Julia overrides them).

    Validation:

    * ``aicdiff`` and ``pvaictest`` are mutually exclusive.
    * ``usertype`` (when set) must be one of the documented X-13 effect
      types; vector form's length must match the number of user series.
    * ``aictest`` (when set) must be in the documented set of testable
      effects.
    * Per-variable ``n`` bounds checked for ``tdstock`` / ``easter`` /
      ``labor`` / ``thank`` / ``sceaster`` / ``easterstock``.
    * Overlapping ``aos`` / ``lss`` ranges emit a :class:`UserWarning`.
    """
    if savelog is None:
        savelog = ["aictest", "chi2test"]

    start: MIT | X13default = _X13DEFAULT
    user: str | list[str] | X13default = _X13DEFAULT
    if not isinstance(data, X13default):
        start = data.range.first()
        names = list(data.column_names)
        user = names[0] if len(names) == 1 else names

    if not isinstance(aicdiff, X13default) and not isinstance(pvaictest, X13default):
        msg = (
            "The aicdiff argument cannot be used in the same regression spec "
            "as the pvaictest argument."
        )
        raise ValueError(msg)

    if not isinstance(usertype, X13default):
        if (
            isinstance(usertype, list)
            and isinstance(user, list)
            and len(usertype) > 1
            and len(usertype) != len(user)
        ):
            msg = (
                f"The usertype argument must have the same length as "
                f"the number of user series provided ({len(user)}) when "
                f"more than a single type is specified. "
                f"Received: {usertype}"
            )
            raise ValueError(msg)
        if isinstance(usertype, list):
            bad = [u for u in usertype if u not in _REGRESSION_USERTYPE_ALLOWED]
            if bad:
                msg = (
                    f"The usertype argument can only have the following "
                    f"values: {sorted(_REGRESSION_USERTYPE_ALLOWED)}. "
                    f"Received: {usertype}"
                )
                raise ValueError(msg)
        elif isinstance(usertype, str) and usertype not in _REGRESSION_USERTYPE_ALLOWED:
            msg = (
                f"The usertype argument can only have the following values: "
                f"{sorted(_REGRESSION_USERTYPE_ALLOWED)}. "
                f"Received: {usertype}"
            )
            raise ValueError(msg)

    if not isinstance(variables, X13default):
        vars_list: list[_VariableArg] = (
            list(variables) if isinstance(variables, list) else [variables]
        )
        for v in vars_list:
            if isinstance(v, X13var):
                _check_calendar_variable_bounds(v)
        _check_outlier_overlaps(vars_list)

    if isinstance(aictest, str):
        if aictest not in _REGRESSION_AICTEST_ALLOWED:
            msg = (
                f"aictest can only contain these entries: "
                f"{sorted(_REGRESSION_AICTEST_ALLOWED)}. Received: {aictest}."
            )
            raise ValueError(msg)
    elif isinstance(aictest, list):
        bad_aic = [a for a in aictest if a not in _REGRESSION_AICTEST_ALLOWED]
        if bad_aic:
            msg = (
                f"aictest can only contain these entries: "
                f"{sorted(_REGRESSION_AICTEST_ALLOWED)}. Received: {aictest}."
            )
            raise ValueError(msg)

    print = _expand_all(print, _REGRESSION_PRINT_ALL)
    save = _expand_all(save, _REGRESSION_SAVE_ALL)

    return X13regression(
        aicdiff=aicdiff,
        aictest=aictest,
        chi2test=chi2test,
        chi2testcv=chi2testcv,
        data=data,
        file=file,
        format=format,
        print=print,
        save=save,
        savelog=savelog,
        pvaictest=pvaictest,
        start=start,
        testalleaster=testalleaster,
        tlimit=tlimit,
        user=user,
        usertype=usertype,
        variables=variables,
        b=b,
        fixb=fixb,
        centeruser=centeruser,
        eastermeans=eastermeans,
        noapply=noapply,
        tcrate=tcrate,
    )

forecast

forecast(
    *,
    exclude: int | X13default = _X13DEFAULT,
    lognormal: bool | X13default = _X13DEFAULT,
    maxback: int | X13default = _X13DEFAULT,
    maxlead: int | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    probability: float | X13default = _X13DEFAULT,
) -> X13forecast

Build the forecast spec — forecast / backcast options.

Mirrors x13spec.jl:1409-1429. Expands print="all" / save="all" to the upstream-defined "everything" lists; otherwise pass-through.

Source code in src/tsecon/x13/_spec.py
def forecast(
    *,
    exclude: int | X13default = _X13DEFAULT,
    lognormal: bool | X13default = _X13DEFAULT,
    maxback: int | X13default = _X13DEFAULT,
    maxlead: int | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    probability: float | X13default = _X13DEFAULT,
) -> X13forecast:
    """Build the ``forecast`` spec — forecast / backcast options.

    Mirrors ``x13spec.jl:1409-1429``. Expands ``print="all"`` /
    ``save="all"`` to the upstream-defined "everything" lists; otherwise
    pass-through.
    """
    print = _expand_all(print, _FORECAST_PRINT_ALL)
    save = _expand_all(save, _FORECAST_SAVE_ALL)
    return X13forecast(
        exclude=exclude,
        lognormal=lognormal,
        maxback=maxback,
        maxlead=maxlead,
        print=print,
        save=save,
        probability=probability,
    )

seats

seats(
    *,
    appendfcst: bool | X13default = _X13DEFAULT,
    finite: bool | X13default = _X13DEFAULT,
    hpcycle: bool | X13default = _X13DEFAULT,
    noadmiss: bool | X13default = _X13DEFAULT,
    out: int | X13default = 0,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    printphtrf: bool | X13default = _X13DEFAULT,
    qmax: int | X13default = _X13DEFAULT,
    statseas: bool | X13default = _X13DEFAULT,
    tabtables: list[str] | X13default = _X13DEFAULT,
    bias: int | X13default = _X13DEFAULT,
    epsiv: float | X13default = _X13DEFAULT,
    epsphi: int | X13default = _X13DEFAULT,
    hplan: int | X13default = _X13DEFAULT,
    imean: bool | X13default = _X13DEFAULT,
    maxit: int | X13default = _X13DEFAULT,
    rmod: float | X13default = _X13DEFAULT,
    xl: float | X13default = _X13DEFAULT,
) -> X13seats

Build the seats spec — SEATS signal-extraction options.

Mirrors x13spec.jl:2478-2527. The upstream's savelog default is the empty list to keep the binary happy on writethrough (see the savelog = _X13default line at x13spec.jl:2513); we honour that and surface savelog as :data:_X13DEFAULT internally rather than as a user-tunable kwarg.

Validation:

  • epsiv must be > 0.
  • Setting hplan while hpcycle=False emits a warning (HP filters are applied regardless because hplan is set).
  • print="all" is rejected (upstream Julia warns + drops); we raise :exc:ValueError to surface the misuse explicitly under the project's error::UserWarning filter.
  • save="all" expands to the SEATS-specific everything list AND forces out=0 (mirrors upstream out=0 reset).
Source code in src/tsecon/x13/_spec.py
def seats(
    *,
    appendfcst: bool | X13default = _X13DEFAULT,
    finite: bool | X13default = _X13DEFAULT,
    hpcycle: bool | X13default = _X13DEFAULT,
    noadmiss: bool | X13default = _X13DEFAULT,
    out: int | X13default = 0,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    printphtrf: bool | X13default = _X13DEFAULT,
    qmax: int | X13default = _X13DEFAULT,
    statseas: bool | X13default = _X13DEFAULT,
    tabtables: list[str] | X13default = _X13DEFAULT,
    bias: int | X13default = _X13DEFAULT,
    epsiv: float | X13default = _X13DEFAULT,
    epsphi: int | X13default = _X13DEFAULT,
    hplan: int | X13default = _X13DEFAULT,
    imean: bool | X13default = _X13DEFAULT,
    maxit: int | X13default = _X13DEFAULT,
    rmod: float | X13default = _X13DEFAULT,
    xl: float | X13default = _X13DEFAULT,
) -> X13seats:
    """Build the ``seats`` spec — SEATS signal-extraction options.

    Mirrors ``x13spec.jl:2478-2527``. The upstream's ``savelog`` default
    is the empty list to keep the binary happy on writethrough (see the
    ``savelog = _X13default`` line at ``x13spec.jl:2513``); we honour
    that and surface ``savelog`` as :data:`_X13DEFAULT` internally rather
    than as a user-tunable kwarg.

    Validation:

    * ``epsiv`` must be > 0.
    * Setting ``hplan`` while ``hpcycle=False`` emits a warning (HP
      filters are applied regardless because ``hplan`` is set).
    * ``print="all"`` is rejected (upstream Julia warns + drops); we
      raise :exc:`ValueError` to surface the misuse explicitly under
      the project's ``error::UserWarning`` filter.
    * ``save="all"`` expands to the SEATS-specific everything list AND
      forces ``out=0`` (mirrors upstream ``out=0`` reset).
    """
    if not isinstance(epsiv, X13default) and epsiv <= 0.0:
        msg = f"epsiv should be a small positive number. Received: {epsiv}."
        raise ValueError(msg)

    if (
        not isinstance(hpcycle, X13default)
        and not isinstance(hplan, X13default)
        and hpcycle is False
    ):
        warnings.warn(
            f"Hodrick-Prescott filters will be used even though hpcycle is "
            f"{hpcycle} because an hplan value has been specified.",
            UserWarning,
            stacklevel=2,
        )

    if (isinstance(print, str) and print == "all") or (
        isinstance(print, list) and print == ["all"]
    ):
        msg = (
            "The print='all' option is not available for the seats spec. "
            "Pass an explicit list of table names instead."
        )
        raise ValueError(msg)

    if (isinstance(save, str) and save == "all") or (isinstance(save, list) and save == ["all"]):
        save = list(_SEATS_SAVE_ALL)
        out = 0

    return X13seats(
        appendfcst=appendfcst,
        finite=finite,
        hpcycle=hpcycle,
        noadmiss=noadmiss,
        out=out,
        print=print,
        save=save,
        savelog=_X13DEFAULT,
        printphtrf=printphtrf,
        qmax=qmax,
        statseas=statseas,
        tabtables=tabtables,
        bias=bias,
        epsiv=epsiv,
        epsphi=epsphi,
        hplan=hplan,
        imean=imean,
        maxit=maxit,
        rmod=rmod,
        xl=xl,
    )

x11

x11(
    *,
    appendbcst: bool | X13default = _X13DEFAULT,
    appendfcst: bool | X13default = _X13DEFAULT,
    final: str | list[str] | X13default = _X13DEFAULT,
    mode: str | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    savelog: str | list[str] | X13default | None = None,
    seasonalma: str | list[str] | X13default = _X13DEFAULT,
    sigmalim: list[_FloatOrNone]
    | list[float]
    | X13default = _X13DEFAULT,
    title: str | list[str] | X13default = _X13DEFAULT,
    trendma: int | X13default = _X13DEFAULT,
    type: str | X13default = _X13DEFAULT,
    calendarsigma: str | X13default = _X13DEFAULT,
    centerseasonal: bool | X13default = _X13DEFAULT,
    keepholiday: bool | X13default = _X13DEFAULT,
    print1stpass: bool | X13default = _X13DEFAULT,
    sfshort: bool | X13default = _X13DEFAULT,
    sigmavec: list[str] | X13default = _X13DEFAULT,
    trendic: float | X13default = _X13DEFAULT,
    true7term: bool | X13default = _X13DEFAULT,
) -> X13x11

Build the x11 spec — X-11 enhanced seasonal-adjustment options.

Mirrors x13spec.jl:3180-3228. The upstream's savelog default is "alldiagnostics"; the Python port uses :data:None as the sentinel that triggers that default (per the established pattern in :func:automdl / :func:regression etc.).

Validation:

  • trendma must be an odd integer in [3, 101].
  • sigmavec requires calendarsigma="select" (other values raise :exc:ValueError).
Source code in src/tsecon/x13/_spec.py
def x11(
    *,
    appendbcst: bool | X13default = _X13DEFAULT,
    appendfcst: bool | X13default = _X13DEFAULT,
    final: str | list[str] | X13default = _X13DEFAULT,
    mode: str | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    savelog: str | list[str] | X13default | None = None,
    seasonalma: str | list[str] | X13default = _X13DEFAULT,
    sigmalim: list[_FloatOrNone] | list[float] | X13default = _X13DEFAULT,
    title: str | list[str] | X13default = _X13DEFAULT,
    trendma: int | X13default = _X13DEFAULT,
    type: str | X13default = _X13DEFAULT,
    calendarsigma: str | X13default = _X13DEFAULT,
    centerseasonal: bool | X13default = _X13DEFAULT,
    keepholiday: bool | X13default = _X13DEFAULT,
    print1stpass: bool | X13default = _X13DEFAULT,
    sfshort: bool | X13default = _X13DEFAULT,
    sigmavec: list[str] | X13default = _X13DEFAULT,
    trendic: float | X13default = _X13DEFAULT,
    true7term: bool | X13default = _X13DEFAULT,
) -> X13x11:
    """Build the ``x11`` spec — X-11 enhanced seasonal-adjustment options.

    Mirrors ``x13spec.jl:3180-3228``. The upstream's ``savelog`` default
    is ``"alldiagnostics"``; the Python port uses :data:`None` as the
    sentinel that triggers that default (per the established pattern in
    :func:`automdl` / :func:`regression` etc.).

    Validation:

    * ``trendma`` must be an odd integer in ``[3, 101]``.
    * ``sigmavec`` requires ``calendarsigma="select"`` (other values
      raise :exc:`ValueError`).
    """
    if savelog is None:
        savelog = "alldiagnostics"

    if not isinstance(trendma, X13default) and (trendma % 2 != 1 or trendma < 3 or trendma > 101):
        msg = f"trendma must be an odd number between 3 and 101. Received: {trendma}."
        raise ValueError(msg)

    if not isinstance(sigmavec, X13default) and (
        isinstance(calendarsigma, X13default)
        or (isinstance(calendarsigma, str) and calendarsigma != "select")
    ):
        msg = "The sigmavec argument can only be specified when calendarsigma='select'."
        raise ValueError(msg)

    print = _expand_all(print, _X11_PRINT_ALL)
    save = _expand_all(save, _X11_SAVE_ALL)

    return X13x11(
        appendbcst=appendbcst,
        appendfcst=appendfcst,
        final=final,
        mode=mode,
        print=print,
        save=save,
        savelog=savelog,
        seasonalma=seasonalma,
        sigmalim=sigmalim,
        title=title,
        trendma=trendma,
        type=type,
        calendarsigma=calendarsigma,
        centerseasonal=centerseasonal,
        keepholiday=keepholiday,
        print1stpass=print1stpass,
        sfshort=sfshort,
        sigmavec=sigmavec,
        trendic=trendic,
        true7term=true7term,
    )

check

check(
    *,
    maxlag: int | X13default = _X13DEFAULT,
    qtype: str | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    savelog: str | list[str] | X13default | None = None,
    acflimit: float | X13default = _X13DEFAULT,
    qlimit: float | X13default = _X13DEFAULT,
) -> X13check

Build the check spec — residual ACF / Ljung-Box diagnostics.

Mirrors x13spec.jl:1149-1169. Expands print="all" / save="all" to the upstream-defined "everything" lists; otherwise pass-through. No numeric range validation in the upstream — :exc:ValueError surfaces from the writer if X-13 rejects the values.

Source code in src/tsecon/x13/_spec.py
def check(
    *,
    maxlag: int | X13default = _X13DEFAULT,
    qtype: str | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    savelog: str | list[str] | X13default | None = None,
    acflimit: float | X13default = _X13DEFAULT,
    qlimit: float | X13default = _X13DEFAULT,
) -> X13check:
    """Build the ``check`` spec — residual ACF / Ljung-Box diagnostics.

    Mirrors ``x13spec.jl:1149-1169``. Expands ``print="all"`` /
    ``save="all"`` to the upstream-defined "everything" lists; otherwise
    pass-through. No numeric range validation in the upstream — :exc:`ValueError`
    surfaces from the writer if X-13 rejects the values.
    """
    if savelog is None:
        savelog = "alldiagnostics"
    print = _expand_all(print, _CHECK_PRINT_ALL)
    save = _expand_all(save, _CHECK_SAVE_ALL)
    return X13check(
        maxlag=maxlag,
        qtype=qtype,
        print=print,
        save=save,
        savelog=savelog,
        acflimit=acflimit,
        qlimit=qlimit,
    )

estimate

estimate(
    *,
    exact: str | X13default = _X13DEFAULT,
    maxiter: int | X13default = _X13DEFAULT,
    outofsample: bool | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    savelog: str | list[str] | X13default | None = None,
    tol: float | X13default = _X13DEFAULT,
    file: str | X13default = _X13DEFAULT,
    fix: str | X13default = _X13DEFAULT,
) -> X13estimate

Build the estimate spec — regARIMA estimation options.

Mirrors x13spec.jl:1228-1251. Expands print="all" / save="all" to the upstream-defined "everything" lists.

Source code in src/tsecon/x13/_spec.py
def estimate(
    *,
    exact: str | X13default = _X13DEFAULT,
    maxiter: int | X13default = _X13DEFAULT,
    outofsample: bool | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    savelog: str | list[str] | X13default | None = None,
    tol: float | X13default = _X13DEFAULT,
    file: str | X13default = _X13DEFAULT,
    fix: str | X13default = _X13DEFAULT,
) -> X13estimate:
    """Build the ``estimate`` spec — regARIMA estimation options.

    Mirrors ``x13spec.jl:1228-1251``. Expands ``print="all"`` /
    ``save="all"`` to the upstream-defined "everything" lists.
    """
    if savelog is None:
        savelog = "alldiagnostics"
    print = _expand_all(print, _ESTIMATE_PRINT_ALL)
    save = _expand_all(save, _ESTIMATE_SAVE_ALL)
    return X13estimate(
        exact=exact,
        maxiter=maxiter,
        outofsample=outofsample,
        print=print,
        save=save,
        savelog=savelog,
        tol=tol,
        file=file,
        fix=fix,
    )

force

force(
    *,
    lambda_: float | X13default = _X13DEFAULT,
    mode: str | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    rho: float | X13default = _X13DEFAULT,
    round: bool | X13default = _X13DEFAULT,
    start: str | X13default = _X13DEFAULT,
    target: str | X13default = _X13DEFAULT,
    type: str | X13default = _X13DEFAULT,
    usefcst: bool | X13default = _X13DEFAULT,
    indforce: bool | X13default = _X13DEFAULT,
) -> X13force

Build the force spec — yearly-total forcing options.

Mirrors x13spec.jl:1343-1372. The Julia lambda keyword renames to lambda_ (Python's reserved word); round and type keep their X-13 names (not Python reserved words, only built-in shadows).

Validation:

  • rho must be in [0.0, 1.0].
Source code in src/tsecon/x13/_spec.py
def force(
    *,
    lambda_: float | X13default = _X13DEFAULT,
    mode: str | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    rho: float | X13default = _X13DEFAULT,
    round: bool | X13default = _X13DEFAULT,
    start: str | X13default = _X13DEFAULT,
    target: str | X13default = _X13DEFAULT,
    type: str | X13default = _X13DEFAULT,
    usefcst: bool | X13default = _X13DEFAULT,
    indforce: bool | X13default = _X13DEFAULT,
) -> X13force:
    """Build the ``force`` spec — yearly-total forcing options.

    Mirrors ``x13spec.jl:1343-1372``. The Julia ``lambda`` keyword renames
    to ``lambda_`` (Python's reserved word); ``round`` and ``type`` keep
    their X-13 names (not Python reserved words, only built-in shadows).

    Validation:

    * ``rho`` must be in ``[0.0, 1.0]``.
    """
    if not isinstance(rho, X13default) and (rho < 0.0 or rho > 1.0):
        msg = f"rho must be between 0 and 1. Received: {rho}."
        raise ValueError(msg)
    print = _expand_all(print, _FORCE_PRINT_ALL)
    save = _expand_all(save, _FORCE_SAVE_ALL)
    return X13force(
        lambda_=lambda_,
        mode=mode,
        print=print,
        save=save,
        rho=rho,
        round=round,
        start=start,
        target=target,
        type=type,
        usefcst=usefcst,
        indforce=indforce,
    )

history

history(
    *,
    endtable: MIT | X13default = _X13DEFAULT,
    estimates: str | list[str] | X13default = _X13DEFAULT,
    fixmdl: bool | X13default = _X13DEFAULT,
    fixreg: bool | X13default = _X13DEFAULT,
    fstep: int | list[int] | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    savelog: str | list[str] | X13default | None = None,
    sadjlags: int | list[int] | X13default = _X13DEFAULT,
    start: MIT | X13default = _X13DEFAULT,
    target: str | X13default = _X13DEFAULT,
    trendlags: int | list[int] | X13default = _X13DEFAULT,
    fixx11reg: bool | X13default = _X13DEFAULT,
    outlier: str | X13default = _X13DEFAULT,
    outlierwin: int | X13default = _X13DEFAULT,
    refresh: bool | X13default = _X13DEFAULT,
    transformfcst: bool | X13default = _X13DEFAULT,
    x11outlier: bool | X13default = _X13DEFAULT,
) -> X13history

Build the history spec — truncated-series revisions analysis.

Mirrors x13spec.jl:1602-1656.

Validation:

  • fstep (when a list) must have length ≤ 4 with every entry ≥ 1. Scalar fstep must be ≥ 1.
  • sadjlags (when a list) must have length ≤ 5 with every entry ≥ 1. Scalar sadjlags must be ≥ 1.
Source code in src/tsecon/x13/_spec.py
def history(
    *,
    endtable: MIT | X13default = _X13DEFAULT,
    estimates: str | list[str] | X13default = _X13DEFAULT,
    fixmdl: bool | X13default = _X13DEFAULT,
    fixreg: bool | X13default = _X13DEFAULT,
    fstep: int | list[int] | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    savelog: str | list[str] | X13default | None = None,
    sadjlags: int | list[int] | X13default = _X13DEFAULT,
    start: MIT | X13default = _X13DEFAULT,
    target: str | X13default = _X13DEFAULT,
    trendlags: int | list[int] | X13default = _X13DEFAULT,
    fixx11reg: bool | X13default = _X13DEFAULT,
    outlier: str | X13default = _X13DEFAULT,
    outlierwin: int | X13default = _X13DEFAULT,
    refresh: bool | X13default = _X13DEFAULT,
    transformfcst: bool | X13default = _X13DEFAULT,
    x11outlier: bool | X13default = _X13DEFAULT,
) -> X13history:
    """Build the ``history`` spec — truncated-series revisions analysis.

    Mirrors ``x13spec.jl:1602-1656``.

    Validation:

    * ``fstep`` (when a list) must have length ≤ 4 with every entry ≥ 1.
      Scalar ``fstep`` must be ≥ 1.
    * ``sadjlags`` (when a list) must have length ≤ 5 with every entry
      ≥ 1. Scalar ``sadjlags`` must be ≥ 1.
    """
    if savelog is None:
        savelog = ["alldiagnostics"]

    if isinstance(fstep, list):
        if len(fstep) > _HISTORY_FSTEP_MAX_LENGTH:
            msg = f"fstep can contain up to four forecast leads. Received: {fstep}."
            raise ValueError(msg)
        if any(v < 1 for v in fstep):
            msg = f"fstep values cannot be less than one. Received: {fstep}."
            raise ValueError(msg)
    elif isinstance(fstep, int) and not isinstance(fstep, bool) and fstep < 1:
        msg = f"fstep cannot be less than one. Received: {fstep}."
        raise ValueError(msg)

    if isinstance(sadjlags, list):
        if len(sadjlags) > _HISTORY_SADJLAGS_MAX_LENGTH:
            msg = f"sadjlags can contain up to five revision lags. Received: {sadjlags}."
            raise ValueError(msg)
        if any(v < 1 for v in sadjlags):
            msg = f"sadjlags values cannot be less than one. Received: {sadjlags}."
            raise ValueError(msg)
    elif isinstance(sadjlags, int) and not isinstance(sadjlags, bool) and sadjlags < 1:
        msg = f"sadjlags cannot be less than one. Received: {sadjlags}."
        raise ValueError(msg)

    print = _expand_all(print, _HISTORY_PRINT_ALL)
    save = _expand_all(save, _HISTORY_SAVE_ALL)
    return X13history(
        endtable=endtable,
        estimates=estimates,
        fixmdl=fixmdl,
        fixreg=fixreg,
        fstep=fstep,
        print=print,
        save=save,
        savelog=savelog,
        sadjlags=sadjlags,
        start=start,
        target=target,
        trendlags=trendlags,
        fixx11reg=fixx11reg,
        outlier=outlier,
        outlierwin=outlierwin,
        refresh=refresh,
        transformfcst=transformfcst,
        x11outlier=x11outlier,
    )

identify

identify(
    *,
    diff: list[int] | X13default = _X13DEFAULT,
    sdiff: list[int] | X13default = _X13DEFAULT,
    maxlag: int | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
) -> X13identify

Build the identify spec — ARIMA-identification ACFs/PACFs.

Mirrors x13spec.jl:1686-1706. No range validation upstream — the binary applies its own bounds.

Source code in src/tsecon/x13/_spec.py
def identify(
    *,
    diff: list[int] | X13default = _X13DEFAULT,
    sdiff: list[int] | X13default = _X13DEFAULT,
    maxlag: int | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
) -> X13identify:
    """Build the ``identify`` spec — ARIMA-identification ACFs/PACFs.

    Mirrors ``x13spec.jl:1686-1706``. No range validation upstream — the
    binary applies its own bounds.
    """
    print = _expand_all(print, _IDENTIFY_PRINT_ALL)
    save = _expand_all(save, _IDENTIFY_SAVE_ALL)
    return X13identify(
        diff=diff,
        sdiff=sdiff,
        maxlag=maxlag,
        print=print,
        save=save,
    )

metadata

metadata(
    entries: tuple[str, str]
    | list[tuple[str, str]]
    | tuple[tuple[str, str], ...]
    | dict[str, str],
) -> X13metadata

Build the metadata spec — diagnostic-summary key/value entries.

Mirrors x13spec.jl:1721-1748. Accepts a single (key, value) pair, an iterable of pairs, or a dict (the dict form is the Python-idiomatic shape; iteration order preserved per PEP 468).

Validation (mirrors upstream):

  • At most 20 entries.
  • No single key or value exceeds 132 characters.
  • Concatenated keys and concatenated values each ≤ 2000 characters.
Source code in src/tsecon/x13/_spec.py
def metadata(
    entries: tuple[str, str] | list[tuple[str, str]] | tuple[tuple[str, str], ...] | dict[str, str],
) -> X13metadata:
    """Build the ``metadata`` spec — diagnostic-summary key/value entries.

    Mirrors ``x13spec.jl:1721-1748``. Accepts a single ``(key, value)``
    pair, an iterable of pairs, or a ``dict`` (the dict form is the
    Python-idiomatic shape; iteration order preserved per PEP 468).

    Validation (mirrors upstream):

    * At most 20 entries.
    * No single key or value exceeds 132 characters.
    * Concatenated keys and concatenated values each ≤ 2000 characters.
    """
    if isinstance(entries, dict):
        items: tuple[tuple[str, str], ...] = tuple(entries.items())
    elif (
        isinstance(entries, tuple)
        and len(entries) == 2
        and isinstance(entries[0], str)
        and isinstance(entries[1], str)
    ):
        items = (entries,)
    else:
        items = tuple((k, v) for k, v in entries)

    if len(items) > _METADATA_MAX_ENTRIES:
        msg = (
            f"A maximum of {_METADATA_MAX_ENTRIES} metadata entries can be "
            f"specified. Received: {len(items)} entries."
        )
        raise ValueError(msg)

    keys = [k for k, _ in items]
    values = [v for _, v in items]
    if any(len(k) > _METADATA_MAX_KEY_OR_VALUE_LENGTH for k in keys):
        msg = (
            f"Keys in the metadata spec can have a maximum length of "
            f"{_METADATA_MAX_KEY_OR_VALUE_LENGTH} characters."
        )
        raise ValueError(msg)
    total_keys = sum(len(k) for k in keys)
    if total_keys > _METADATA_MAX_TOTAL_LENGTH:
        msg = (
            f"Keys in the metadata spec can have a maximum combined "
            f"length of {_METADATA_MAX_TOTAL_LENGTH} characters. "
            f"Received: {total_keys} characters."
        )
        raise ValueError(msg)
    if any(len(v) > _METADATA_MAX_KEY_OR_VALUE_LENGTH for v in values):
        msg = (
            f"Values in the metadata spec can have a maximum length of "
            f"{_METADATA_MAX_KEY_OR_VALUE_LENGTH} characters."
        )
        raise ValueError(msg)
    total_values = sum(len(v) for v in values)
    if total_values > _METADATA_MAX_TOTAL_LENGTH:
        msg = (
            f"Values in the metadata spec can have a maximum combined "
            f"length of {_METADATA_MAX_TOTAL_LENGTH} characters. "
            f"Received: {total_values} characters."
        )
        raise ValueError(msg)

    return X13metadata(entries=items)

outlier

outlier(
    *,
    critical: float
    | list[float | None]
    | list[float]
    | X13default = _X13DEFAULT,
    lsrun: int | X13default = _X13DEFAULT,
    method: str | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    savelog: str | list[str] | X13default | None = None,
    span: MITRange | Span | X13default = _X13DEFAULT,
    types: str | list[str] | X13default = _X13DEFAULT,
    almost: float | X13default = _X13DEFAULT,
    tcrate: float | X13default = _X13DEFAULT,
) -> X13outlier

Build the outlier spec — automatic outlier detection.

Mirrors x13spec.jl:1833-1881.

Validation:

  • critical (when a list) must have length ≤ 3.
  • lsrun must be in [0, 5].
  • almost must be > 0.
  • tcrate must be in (0.0, 1.0).
  • :class:Span span rejects fuzzy e (M11 / Q2) — mirrored as "endpoints must be MIT or None"; the builder accepts only :class:MIT or :data:None already, so the upstream check collapses to a no-op here.
Source code in src/tsecon/x13/_spec.py
def outlier(
    *,
    critical: float | list[float | None] | list[float] | X13default = _X13DEFAULT,
    lsrun: int | X13default = _X13DEFAULT,
    method: str | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    savelog: str | list[str] | X13default | None = None,
    span: MITRange | Span | X13default = _X13DEFAULT,
    types: str | list[str] | X13default = _X13DEFAULT,
    almost: float | X13default = _X13DEFAULT,
    tcrate: float | X13default = _X13DEFAULT,
) -> X13outlier:
    """Build the ``outlier`` spec — automatic outlier detection.

    Mirrors ``x13spec.jl:1833-1881``.

    Validation:

    * ``critical`` (when a list) must have length ≤ 3.
    * ``lsrun`` must be in ``[0, 5]``.
    * ``almost`` must be > 0.
    * ``tcrate`` must be in ``(0.0, 1.0)``.
    * :class:`Span` ``span`` rejects fuzzy ``e`` (``M11`` / ``Q2``) —
      mirrored as "endpoints must be MIT or None"; the builder accepts
      only :class:`MIT` or :data:`None` already, so the upstream check
      collapses to a no-op here.
    """
    if savelog is None:
        savelog = "identified"

    if isinstance(critical, list) and len(critical) > _OUTLIER_CRITICAL_MAX_LENGTH:
        msg = (
            f"critical can contain up to {_OUTLIER_CRITICAL_MAX_LENGTH} "
            f"values. Received: {critical}."
        )
        raise ValueError(msg)

    if not isinstance(lsrun, X13default) and (lsrun < 0 or lsrun > _OUTLIER_LSRUN_MAX):
        msg = f"lsrun can take values from 0 to {_OUTLIER_LSRUN_MAX}. Received: {lsrun}."
        raise ValueError(msg)

    if not isinstance(almost, X13default) and almost < 0.0:
        msg = f"almost must have a value greater than zero. Received: {almost}."
        raise ValueError(msg)

    if not isinstance(tcrate, X13default) and (tcrate <= 0.0 or tcrate >= 1.0):
        msg = f"tcrate must be a number greater than zero and less than one. Received: {tcrate}."
        raise ValueError(msg)

    print = _expand_all(print, _OUTLIER_PRINT_ALL)
    save = _expand_all(save, _OUTLIER_SAVE_ALL)
    return X13outlier(
        critical=critical,
        lsrun=lsrun,
        method=method,
        print=print,
        save=save,
        savelog=savelog,
        span=span,
        types=types,
        almost=almost,
        tcrate=tcrate,
    )

pickmdl

pickmdl(
    *models: ArimaModel,
    bcstlim: int | X13default = _X13DEFAULT,
    fcstlim: int | X13default = _X13DEFAULT,
    identify: str | X13default = _X13DEFAULT,
    method: str | X13default = _X13DEFAULT,
    mode: str | X13default = _X13DEFAULT,
    outofsample: bool | X13default = _X13DEFAULT,
    overdiff: float | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    savelog: str | list[str] | X13default | None = None,
    qlim: int | X13default = _X13DEFAULT,
    file: str | X13default = _X13DEFAULT,
) -> X13pickmdl

Build the pickmdl spec — automatic ARIMA model selection.

Mirrors x13spec.jl:1972-2030. The Julia pickmdl(models::Vector{ArimaModel}) and pickmdl(models::ArimaModel...) overloads collapse to a single Python signature with a positional *models varargs — pass either pickmdl(m1, m2) or pickmdl(*[m1, m2]).

The identify kwarg shadows the :func:identify spec builder at module level; this is the X-13 grammar's name (identify=:first / :all) and the per-kwarg A002 ignore preserves the surface.

Validation:

  • Either models (≥ 2 candidates) OR file= must be supplied.
  • At most one candidate may have default=True.
  • bcstlim / fcstlim / qlim[0, 100].
  • overdiff[0.9, 1.0].
Source code in src/tsecon/x13/_spec.py
def pickmdl(
    *models: ArimaModel,
    bcstlim: int | X13default = _X13DEFAULT,
    fcstlim: int | X13default = _X13DEFAULT,
    identify: str | X13default = _X13DEFAULT,
    method: str | X13default = _X13DEFAULT,
    mode: str | X13default = _X13DEFAULT,
    outofsample: bool | X13default = _X13DEFAULT,
    overdiff: float | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    savelog: str | list[str] | X13default | None = None,
    qlim: int | X13default = _X13DEFAULT,
    file: str | X13default = _X13DEFAULT,
) -> X13pickmdl:
    """Build the ``pickmdl`` spec — automatic ARIMA model selection.

    Mirrors ``x13spec.jl:1972-2030``. The Julia ``pickmdl(models::Vector{ArimaModel})``
    and ``pickmdl(models::ArimaModel...)`` overloads collapse to a single
    Python signature with a positional ``*models`` varargs — pass either
    ``pickmdl(m1, m2)`` or ``pickmdl(*[m1, m2])``.

    The ``identify`` kwarg shadows the :func:`identify` spec builder at
    module level; this is the X-13 grammar's name (``identify=:first`` /
    ``:all``) and the per-kwarg ``A002`` ignore preserves the surface.

    Validation:

    * Either ``models`` (≥ 2 candidates) OR ``file=`` must be supplied.
    * At most one candidate may have ``default=True``.
    * ``bcstlim`` / ``fcstlim`` / ``qlim`` ∈ ``[0, 100]``.
    * ``overdiff`` ∈ ``[0.9, 1.0]``.
    """
    if savelog is None:
        savelog = "automodel"

    if not isinstance(bcstlim, X13default) and (bcstlim < 0 or bcstlim > _PICKMDL_PERCENT_MAX):
        msg = (
            f"bcstlim must be a value between 0 and {_PICKMDL_PERCENT_MAX} "
            f"(inclusive). Received: {bcstlim}."
        )
        raise ValueError(msg)
    if not isinstance(fcstlim, X13default) and (fcstlim < 0 or fcstlim > _PICKMDL_PERCENT_MAX):
        msg = (
            f"fcstlim must be a value between 0 and {_PICKMDL_PERCENT_MAX} "
            f"(inclusive). Received: {fcstlim}."
        )
        raise ValueError(msg)
    if not isinstance(qlim, X13default) and (qlim < 0 or qlim > _PICKMDL_PERCENT_MAX):
        msg = (
            f"qlim must be a value between 0 and {_PICKMDL_PERCENT_MAX} "
            f"(inclusive). Received: {qlim}."
        )
        raise ValueError(msg)

    if not isinstance(overdiff, X13default):
        if overdiff > _PICKMDL_OVERDIFF_MAX:
            msg = f"overdiff must not be greater than 1. Received: {overdiff}."
            raise ValueError(msg)
        if overdiff < _PICKMDL_OVERDIFF_MIN:
            msg = f"overdiff should not be less than {_PICKMDL_OVERDIFF_MIN}. Received: {overdiff}."
            raise ValueError(msg)

    models_value: list[ArimaModel] | X13default
    if models:
        if len(models) < _PICKMDL_MIN_MODELS:
            msg = (
                f"pickmdl spec must be provided with at least "
                f"{_PICKMDL_MIN_MODELS} candidate models. "
                f"Received: {len(models)}. {list(models)}"
            )
            raise ValueError(msg)
        num_defaults = sum(1 for m in models if m.default)
        if num_defaults > 1:
            msg = (
                f"pickmdl can only have one model specified as a default, "
                f"but {num_defaults} of the provided models are flagged "
                f"as defaults."
            )
            raise ValueError(msg)
        models_value = list(models)
    else:
        models_value = _X13DEFAULT
        if isinstance(file, X13default):
            msg = (
                "pickmdl spec must either be constructed with one or more "
                "ArimaModels or with the file keyword argument specified."
            )
            raise ValueError(msg)

    print = _expand_all(print, _PICKMDL_PRINT_ALL)
    return X13pickmdl(
        bcstlim=bcstlim,
        fcstlim=fcstlim,
        models=models_value,
        identify=identify,
        method=method,
        mode=mode,
        outofsample=outofsample,
        overdiff=overdiff,
        print=print,
        savelog=savelog,
        qlim=qlim,
        file=file,
    )

slidingspans

slidingspans(
    *,
    cutchng: float | X13default = _X13DEFAULT,
    cutseas: float | X13default = _X13DEFAULT,
    cuttd: float | X13default = _X13DEFAULT,
    fixmdl: bool | str | X13default = _X13DEFAULT,
    fixreg: list[str] | X13default = _X13DEFAULT,
    length: int | X13default = _X13DEFAULT,
    numspans: int | X13default = _X13DEFAULT,
    outlier: str | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    savelog: str | list[str] | X13default | None = None,
    start: MIT | X13default = _X13DEFAULT,
    additivesa: str | X13default = _X13DEFAULT,
    fixx11reg: bool | X13default = _X13DEFAULT,
    x11outlier: bool | X13default = _X13DEFAULT,
) -> X13slidingspans

Build the slidingspans spec — stability analysis options.

Mirrors x13spec.jl:2666-2701. The Julia length field collides with Python's :func:len built-in but is not a Python reserved word, so the kwarg keeps its X-13 name with a per-line A002 ignore.

Validation:

  • Setting both fixmdl=True and fixreg=[...] warns (fixreg is ignored by X-13 in that combination, mirroring the upstream @warn).
Source code in src/tsecon/x13/_spec.py
def slidingspans(
    *,
    cutchng: float | X13default = _X13DEFAULT,
    cutseas: float | X13default = _X13DEFAULT,
    cuttd: float | X13default = _X13DEFAULT,
    fixmdl: bool | str | X13default = _X13DEFAULT,
    fixreg: list[str] | X13default = _X13DEFAULT,
    length: int | X13default = _X13DEFAULT,
    numspans: int | X13default = _X13DEFAULT,
    outlier: str | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    savelog: str | list[str] | X13default | None = None,
    start: MIT | X13default = _X13DEFAULT,
    additivesa: str | X13default = _X13DEFAULT,
    fixx11reg: bool | X13default = _X13DEFAULT,
    x11outlier: bool | X13default = _X13DEFAULT,
) -> X13slidingspans:
    """Build the ``slidingspans`` spec — stability analysis options.

    Mirrors ``x13spec.jl:2666-2701``. The Julia ``length`` field collides
    with Python's :func:`len` built-in but is not a Python reserved word,
    so the kwarg keeps its X-13 name with a per-line ``A002`` ignore.

    Validation:

    * Setting both ``fixmdl=True`` and ``fixreg=[...]`` warns
      (``fixreg`` is ignored by X-13 in that combination, mirroring the
      upstream ``@warn``).
    """
    if savelog is None:
        savelog = "percents"

    if not isinstance(fixmdl, X13default) and not isinstance(fixreg, X13default) and fixmdl is True:
        warnings.warn(
            "fixreg will be ignored because fixmdl is set to true.",
            UserWarning,
            stacklevel=2,
        )

    print = _expand_all(print, _SLIDINGSPANS_PRINT_ALL)
    save = _expand_all(save, _SLIDINGSPANS_SAVE_ALL)
    return X13slidingspans(
        cutchng=cutchng,
        cutseas=cutseas,
        cuttd=cuttd,
        fixmdl=fixmdl,
        fixreg=fixreg,
        length=length,
        numspans=numspans,
        outlier=outlier,
        print=print,
        save=save,
        savelog=savelog,
        start=start,
        additivesa=additivesa,
        fixx11reg=fixx11reg,
        x11outlier=x11outlier,
    )

spectrum

spectrum(
    *,
    logqs: bool | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    savelog: str | list[str] | X13default | None = None,
    qcheck: bool | X13default = _X13DEFAULT,
    start: MIT | X13default = _X13DEFAULT,
    tukey120: bool | X13default = _X13DEFAULT,
    decibel: bool | X13default = _X13DEFAULT,
    difference: bool | str | X13default = _X13DEFAULT,
    maxar: int | X13default = _X13DEFAULT,
    peakwidth: int | X13default = _X13DEFAULT,
    series: str | X13default = _X13DEFAULT,
    siglevel: int | X13default = _X13DEFAULT,
    type: str | X13default = _X13DEFAULT,
) -> X13spectrum

Build the spectrum spec — spectral-diagnostic options.

Mirrors x13spec.jl:2785-2816. No numeric validation upstream; range checks happen in the X-13 binary.

The series kwarg shadows the :func:series spec builder at module level; this is the X-13 grammar's name (series=:original selects which series feeds the spectrum) and the per-kwarg A002 ignore preserves the surface.

Source code in src/tsecon/x13/_spec.py
def spectrum(
    *,
    logqs: bool | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    savelog: str | list[str] | X13default | None = None,
    qcheck: bool | X13default = _X13DEFAULT,
    start: MIT | X13default = _X13DEFAULT,
    tukey120: bool | X13default = _X13DEFAULT,
    decibel: bool | X13default = _X13DEFAULT,
    difference: bool | str | X13default = _X13DEFAULT,
    maxar: int | X13default = _X13DEFAULT,
    peakwidth: int | X13default = _X13DEFAULT,
    series: str | X13default = _X13DEFAULT,
    siglevel: int | X13default = _X13DEFAULT,
    type: str | X13default = _X13DEFAULT,
) -> X13spectrum:
    """Build the ``spectrum`` spec — spectral-diagnostic options.

    Mirrors ``x13spec.jl:2785-2816``. No numeric validation upstream;
    range checks happen in the X-13 binary.

    The ``series`` kwarg shadows the :func:`series` spec builder at
    module level; this is the X-13 grammar's name (``series=:original``
    selects which series feeds the spectrum) and the per-kwarg ``A002``
    ignore preserves the surface.
    """
    if savelog is None:
        savelog = "alldiagnostics"
    print = _expand_all(print, _SPECTRUM_PRINT_ALL)
    save = _expand_all(save, _SPECTRUM_SAVE_ALL)
    return X13spectrum(
        logqs=logqs,
        print=print,
        save=save,
        savelog=savelog,
        qcheck=qcheck,
        start=start,
        tukey120=tukey120,
        decibel=decibel,
        difference=difference,
        maxar=maxar,
        peakwidth=peakwidth,
        series=series,
        siglevel=siglevel,
        type=type,
    )

x11regression

x11regression(
    *,
    aicdiff: float | X13default = _X13DEFAULT,
    aictest: str | list[str] | X13default = _X13DEFAULT,
    critical: float | X13default = _X13DEFAULT,
    data: MVTSeries | X13default = _X13DEFAULT,
    file: str | X13default = _X13DEFAULT,
    format: str | X13default = _X13DEFAULT,
    outliermethod: str | X13default = _X13DEFAULT,
    outlierspan: MITRange | Span | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    savelog: str | list[str] | X13default | None = None,
    prior: bool | X13default = _X13DEFAULT,
    sigma: float | X13default = _X13DEFAULT,
    span: MITRange | Span | X13default = _X13DEFAULT,
    tdprior: list[float] | X13default = _X13DEFAULT,
    usertype: str | list[str] | X13default = _X13DEFAULT,
    variables: _VariablesField = _X13DEFAULT,
    almost: float | X13default = _X13DEFAULT,
    b: list[float] | X13default = _X13DEFAULT,
    fixb: list[bool] | X13default = _X13DEFAULT,
    centeruser: str | X13default = _X13DEFAULT,
    eastermeans: bool | X13default = _X13DEFAULT,
    forcecal: bool | X13default = _X13DEFAULT,
    noapply: list[str] | X13default = _X13DEFAULT,
    reweight: bool | X13default = _X13DEFAULT,
    umdata: MVTSeries | X13default = _X13DEFAULT,
    umfile: str | X13default = _X13DEFAULT,
    umformat: str | X13default = _X13DEFAULT,
    umprecision: int | X13default = _X13DEFAULT,
    umtrimzero: bool | str | X13default = _X13DEFAULT,
) -> X13x11regression

Build the x11regression spec — calendar / outlier irregular-component regression.

Mirrors x13spec.jl:3420-3559. The start / user / umstart / umname fields are derived from data / umdata (mirrors the Julia upstream's derivation; no user-facing kwargs accepted).

Validation:

  • aictest (when set) restricted to {td, tdstock, td1coef, tdstock1coef, easter, user}. If both a TD aictest entry and a TD variables entry are present, the AIC-test set must be a subset of the variables-used set.
  • sigma must be > 0.
  • tdprior must have length 7 and all entries ≥ 0.
  • usertype (when set) restricted to {td, holiday, user}. Vector form's length must match the number of user series.
  • :class:Span outlierspan rejects fuzzy e (the builder accepts only :class:MIT or :data:None already; the upstream check collapses to a no-op).
Source code in src/tsecon/x13/_spec.py
def x11regression(  # noqa: PLR0912, PLR0915
    *,
    aicdiff: float | X13default = _X13DEFAULT,
    aictest: str | list[str] | X13default = _X13DEFAULT,
    critical: float | X13default = _X13DEFAULT,
    data: MVTSeries | X13default = _X13DEFAULT,
    file: str | X13default = _X13DEFAULT,
    format: str | X13default = _X13DEFAULT,
    outliermethod: str | X13default = _X13DEFAULT,
    outlierspan: MITRange | Span | X13default = _X13DEFAULT,
    print: str | list[str] | X13default = _X13DEFAULT,
    save: str | list[str] | X13default = _X13DEFAULT,
    savelog: str | list[str] | X13default | None = None,
    prior: bool | X13default = _X13DEFAULT,
    sigma: float | X13default = _X13DEFAULT,
    span: MITRange | Span | X13default = _X13DEFAULT,
    tdprior: list[float] | X13default = _X13DEFAULT,
    usertype: str | list[str] | X13default = _X13DEFAULT,
    variables: _VariablesField = _X13DEFAULT,
    almost: float | X13default = _X13DEFAULT,
    b: list[float] | X13default = _X13DEFAULT,
    fixb: list[bool] | X13default = _X13DEFAULT,
    centeruser: str | X13default = _X13DEFAULT,
    eastermeans: bool | X13default = _X13DEFAULT,
    forcecal: bool | X13default = _X13DEFAULT,
    noapply: list[str] | X13default = _X13DEFAULT,
    reweight: bool | X13default = _X13DEFAULT,
    umdata: MVTSeries | X13default = _X13DEFAULT,
    umfile: str | X13default = _X13DEFAULT,
    umformat: str | X13default = _X13DEFAULT,
    umprecision: int | X13default = _X13DEFAULT,
    umtrimzero: bool | str | X13default = _X13DEFAULT,
) -> X13x11regression:
    """Build the ``x11regression`` spec — calendar / outlier irregular-component regression.

    Mirrors ``x13spec.jl:3420-3559``. The ``start`` / ``user`` / ``umstart`` /
    ``umname`` fields are derived from ``data`` / ``umdata`` (mirrors the
    Julia upstream's derivation; no user-facing kwargs accepted).

    Validation:

    * ``aictest`` (when set) restricted to ``{td, tdstock, td1coef,
      tdstock1coef, easter, user}``. If both a TD ``aictest`` entry and a
      TD ``variables`` entry are present, the AIC-test set must be a
      subset of the variables-used set.
    * ``sigma`` must be > 0.
    * ``tdprior`` must have length 7 and all entries ≥ 0.
    * ``usertype`` (when set) restricted to ``{td, holiday, user}``.
      Vector form's length must match the number of user series.
    * :class:`Span` ``outlierspan`` rejects fuzzy ``e`` (the builder
      accepts only :class:`MIT` or :data:`None` already; the upstream
      check collapses to a no-op).
    """
    if savelog is None:
        savelog = "aictest"

    start: MIT | X13default = _X13DEFAULT
    user: str | list[str] | X13default = _X13DEFAULT
    if not isinstance(data, X13default):
        start = data.range.first()
        names = list(data.column_names)
        user = names[0] if len(names) == 1 else names

    umstart: MIT | X13default = _X13DEFAULT
    umname: list[str] | str | X13default = _X13DEFAULT
    if not isinstance(umdata, X13default):
        umstart = umdata.range.first()
        umnames = list(umdata.column_names)
        umname = umnames[0] if len(umnames) == 1 else umnames

    if not isinstance(variables, X13default) and not isinstance(aictest, X13default):
        vars_list: list[_VariableArg] = (
            list(variables) if isinstance(variables, list) else [variables]
        )
        aics_list = [aictest] if isinstance(aictest, str) else list(aictest)
        types_used = {_x11regression_variable_type(v) for v in vars_list}
        has_td_in_aics = any(a in _X11REGRESSION_TD_AIC_TYPES for a in aics_list)
        has_td_in_vars = bool(types_used & _X11REGRESSION_TD_AIC_TYPES)
        if has_td_in_aics and has_td_in_vars:
            for aic in aics_list:
                if aic in _X11REGRESSION_TD_AIC_TYPES and aic not in types_used:
                    msg = (
                        f"Trading day regressors specified in the aictest "
                        f"must correspond with trading day regressors "
                        f"provided in the variables argument. {aic} was "
                        f"specified in the aictest argument, but the "
                        f"variables argument uses "
                        f"{sorted(types_used & _X11REGRESSION_TD_AIC_TYPES)}."
                    )
                    raise ValueError(msg)

    if isinstance(aictest, str):
        if aictest not in _X11REGRESSION_AICTEST_ALLOWED:
            msg = (
                f"aictest can only contain these entries: "
                f"{sorted(_X11REGRESSION_AICTEST_ALLOWED)}. "
                f"Received: {aictest}."
            )
            raise ValueError(msg)
    elif isinstance(aictest, list):
        bad_aic = [a for a in aictest if a not in _X11REGRESSION_AICTEST_ALLOWED]
        if bad_aic:
            msg = (
                f"aictest can only contain these entries: "
                f"{sorted(_X11REGRESSION_AICTEST_ALLOWED)}. "
                f"Received: {aictest}."
            )
            raise ValueError(msg)

    if not isinstance(sigma, X13default) and sigma <= 0.0:
        msg = f"sigma must be a number greater than 0. Received: {sigma}."
        raise ValueError(msg)

    if not isinstance(tdprior, X13default):
        if len(tdprior) != _X11REGRESSION_TDPRIOR_LENGTH:
            msg = (
                f"tdprior must have a length of exactly "
                f"{_X11REGRESSION_TDPRIOR_LENGTH}. Received: {tdprior}."
            )
            raise ValueError(msg)
        if any(x < 0.0 for x in tdprior):
            msg = f"tdprior values must all be greater than or equal to 0. Received: {tdprior}."
            raise ValueError(msg)

    if not isinstance(usertype, X13default):
        if (
            isinstance(usertype, list)
            and isinstance(user, list)
            and len(usertype) > 1
            and len(usertype) != len(user)
        ):
            msg = (
                f"The usertype argument must have the same length as "
                f"the number of user series provided ({len(user)}) when "
                f"more than a single type is specified. "
                f"Received: {usertype}"
            )
            raise ValueError(msg)
        if isinstance(usertype, list):
            bad = [u for u in usertype if u not in _X11REGRESSION_USERTYPE_ALLOWED]
            if bad:
                msg = (
                    f"The usertype argument can only have the following "
                    f"values: {sorted(_X11REGRESSION_USERTYPE_ALLOWED)}. "
                    f"\n\nReceived: {usertype}"
                )
                raise ValueError(msg)
        elif isinstance(usertype, str) and usertype not in _X11REGRESSION_USERTYPE_ALLOWED:
            msg = (
                f"The usertype argument can only have the following "
                f"values: {sorted(_X11REGRESSION_USERTYPE_ALLOWED)}. "
                f"\n\nReceived: {usertype}"
            )
            raise ValueError(msg)

    print = _expand_all(print, _X11REGRESSION_PRINT_ALL)
    save = _expand_all(save, _X11REGRESSION_SAVE_ALL)
    return X13x11regression(
        aicdiff=aicdiff,
        aictest=aictest,
        critical=critical,
        data=data,
        file=file,
        format=format,
        outliermethod=outliermethod,
        outlierspan=outlierspan,
        print=print,
        save=save,
        savelog=savelog,
        prior=prior,
        sigma=sigma,
        span=span,
        start=start,
        tdprior=tdprior,
        user=user,
        usertype=usertype,
        variables=variables,
        almost=almost,
        b=b,
        fixb=fixb,
        centeruser=centeruser,
        eastermeans=eastermeans,
        forcecal=forcecal,
        noapply=noapply,
        reweight=reweight,
        umdata=umdata,
        umfile=umfile,
        umformat=umformat,
        umname=umname,
        umprecision=umprecision,
        umstart=umstart,
        umtrimzero=umtrimzero,
    )

newspec

newspec(
    series: X13series | TSeries | X13default = _X13DEFAULT,
    *,
    arima: X13arima | X13default = _X13DEFAULT,
    estimate: X13estimate | X13default = _X13DEFAULT,
    transform: X13transform | X13default = _X13DEFAULT,
    regression: X13regression | X13default = _X13DEFAULT,
    automdl: X13automdl | X13default = _X13DEFAULT,
    x11: X13x11 | X13default = _X13DEFAULT,
    x11regression: X13x11regression
    | X13default = _X13DEFAULT,
    check: X13check | X13default = _X13DEFAULT,
    forecast: X13forecast | X13default = _X13DEFAULT,
    force: X13force | X13default = _X13DEFAULT,
    pickmdl: X13pickmdl | X13default = _X13DEFAULT,
    history: X13history | X13default = _X13DEFAULT,
    metadata: X13metadata | X13default = _X13DEFAULT,
    identify: X13identify | X13default = _X13DEFAULT,
    outlier: X13outlier | X13default = _X13DEFAULT,
    seats: X13seats | X13default = _X13DEFAULT,
    slidingspans: X13slidingspans
    | X13default = _X13DEFAULT,
    spectrum: X13spectrum | X13default = _X13DEFAULT,
    folder: str | X13default = _X13DEFAULT,
) -> X13spec

Build a new :class:X13spec aggregator.

Mirrors x13spec.jl:578-629. The Julia overloads newspec(::X13series), newspec(::Type{<:Frequency}), and newspec(::TSeries) collapse to a single Python signature: pass either an :class:X13series (the usual shape) or a :class:~tsecon.tseries.TSeries (the convenience shape — internally wrapped via :func:series). The Julia newspec(::Type{<:Frequency}) overload, which builds a spec without series for the rare composite-only flow, is intentionally not ported — the composite spec is not supported (see x13spec.jl:563).

The string field is always initialised to :data:_X13DEFAULT — it is populated only by :func:tsecon.x13._write.x13write as a side-effect of serialization (mirrors the Julia upstream's spec.string = ... mutation at x13write.jl:72).

Source code in src/tsecon/x13/_spec.py
def newspec(
    series: X13series | TSeries | X13default = _X13DEFAULT,
    *,
    arima: X13arima | X13default = _X13DEFAULT,
    estimate: X13estimate | X13default = _X13DEFAULT,
    transform: X13transform | X13default = _X13DEFAULT,
    regression: X13regression | X13default = _X13DEFAULT,
    automdl: X13automdl | X13default = _X13DEFAULT,
    x11: X13x11 | X13default = _X13DEFAULT,
    x11regression: X13x11regression | X13default = _X13DEFAULT,
    check: X13check | X13default = _X13DEFAULT,
    forecast: X13forecast | X13default = _X13DEFAULT,
    force: X13force | X13default = _X13DEFAULT,
    pickmdl: X13pickmdl | X13default = _X13DEFAULT,
    history: X13history | X13default = _X13DEFAULT,
    metadata: X13metadata | X13default = _X13DEFAULT,
    identify: X13identify | X13default = _X13DEFAULT,
    outlier: X13outlier | X13default = _X13DEFAULT,
    seats: X13seats | X13default = _X13DEFAULT,
    slidingspans: X13slidingspans | X13default = _X13DEFAULT,
    spectrum: X13spectrum | X13default = _X13DEFAULT,
    folder: str | X13default = _X13DEFAULT,
) -> X13spec:
    """Build a new :class:`X13spec` aggregator.

    Mirrors ``x13spec.jl:578-629``. The Julia overloads
    ``newspec(::X13series)``, ``newspec(::Type{<:Frequency})``, and
    ``newspec(::TSeries)`` collapse to a single Python signature: pass
    either an :class:`X13series` (the usual shape) or a
    :class:`~tsecon.tseries.TSeries` (the convenience shape — internally
    wrapped via :func:`series`). The Julia ``newspec(::Type{<:Frequency})``
    overload, which builds a spec without ``series`` for the rare
    ``composite``-only flow, is **intentionally not ported** — the
    ``composite`` spec is not supported (see ``x13spec.jl:563``).

    The ``string`` field is always initialised to :data:`_X13DEFAULT` —
    it is populated only by :func:`tsecon.x13._write.x13write` as a
    side-effect of serialization (mirrors the Julia upstream's
    ``spec.string = ...`` mutation at ``x13write.jl:72``).
    """
    if isinstance(series, TSeries):
        series = _build_series_block(series)
    return X13spec(
        series=series,
        arima=arima,
        estimate=estimate,
        transform=transform,
        regression=regression,
        automdl=automdl,
        x11=x11,
        x11regression=x11regression,
        check=check,
        forecast=forecast,
        force=force,
        pickmdl=pickmdl,
        history=history,
        metadata=metadata,
        identify=identify,
        outlier=outlier,
        seats=seats,
        slidingspans=slidingspans,
        spectrum=spectrum,
        folder=folder,
        string=_X13DEFAULT,
    )

validateX13spec

validateX13spec(spec: X13spec) -> None

Cross-spec invariant check — runs before the binary sees the spec.

Mirrors x13spec.jl:3563-4055 (validateX13spec). Branches that Julia handles with ArgumentError raise :exc:ValueError; branches that Julia logs via @warn emit :class:UserWarning through :func:warnings.warn (the project's error::UserWarning filter at pyproject.toml turns silent misuse into a test-failure, mirroring the Julia upstream's @test_warn discipline).

Coverage spans:

  • ARIMA-source mutual exclusion: arimaautomdlpickmdl.
  • estimate.file overrides: blocks arima.{model,ar,ma} and regression.{variables,user,b}.
  • Forecast / history overlap: history.fstep ≤ forecast.maxlead.
  • Regression variable type compatibility (~16 raise sites): the trading-day / leap-year / length-of-period / stock-vs-flow compatibility matrix per x13spec.jl:3621-3777.
  • AIC-test type compatibility (~12 raise sites): parallel to the variables matrix but for the aictest= argument.
  • Outlier-range containment: ao / ls / so / tc / range variants must lie within the series range.
  • Regression / x11regression data-range containment (with forecast backcast/maxlead expansion).
  • Slidingspans length bounds (quarterly: 12 ≤ length ≤ 76; monthly: 36 ≤ length ≤ 228).
  • Seats HP-filter sample-size warning.
  • Modelspan-vs-span backcast warning.
  • Transform/x11 mode compatibility (adjust vs x11.mode).
  • X11regression-vs-forcecal warning.
Source code in src/tsecon/x13/_spec.py
4239
4240
4241
4242
4243
4244
4245
4246
4247
4248
4249
4250
4251
4252
4253
4254
4255
4256
4257
4258
4259
4260
4261
4262
4263
4264
4265
4266
4267
4268
4269
4270
4271
4272
4273
4274
4275
4276
4277
4278
4279
4280
4281
4282
4283
4284
4285
4286
4287
4288
4289
4290
4291
4292
4293
4294
4295
4296
4297
4298
4299
4300
4301
4302
4303
4304
4305
4306
4307
4308
4309
4310
4311
4312
4313
4314
4315
4316
4317
4318
4319
4320
4321
4322
4323
4324
4325
4326
4327
4328
4329
4330
4331
4332
4333
4334
4335
4336
4337
4338
4339
4340
4341
4342
4343
4344
4345
4346
4347
4348
4349
4350
4351
4352
4353
4354
4355
4356
4357
4358
4359
4360
4361
4362
4363
4364
4365
4366
4367
4368
4369
4370
4371
4372
4373
4374
4375
4376
4377
4378
4379
4380
4381
4382
4383
4384
4385
4386
4387
4388
4389
4390
4391
4392
4393
4394
4395
4396
4397
4398
4399
4400
4401
4402
4403
4404
4405
4406
4407
4408
4409
4410
4411
4412
4413
4414
4415
4416
4417
4418
4419
4420
4421
4422
4423
4424
4425
4426
4427
4428
4429
4430
4431
4432
4433
4434
4435
4436
4437
4438
4439
4440
4441
4442
4443
4444
4445
4446
4447
4448
4449
4450
4451
4452
4453
4454
4455
4456
4457
4458
4459
4460
4461
4462
4463
4464
4465
4466
4467
4468
4469
4470
4471
4472
4473
4474
4475
4476
4477
4478
4479
4480
4481
4482
4483
4484
4485
4486
4487
4488
4489
4490
4491
4492
4493
4494
4495
4496
4497
4498
4499
4500
4501
4502
4503
4504
4505
4506
4507
4508
4509
4510
4511
4512
4513
4514
4515
4516
4517
4518
4519
4520
4521
4522
4523
4524
4525
4526
4527
4528
4529
4530
4531
4532
4533
4534
4535
4536
4537
4538
4539
4540
4541
4542
4543
4544
4545
4546
4547
4548
4549
4550
4551
4552
4553
4554
4555
4556
4557
4558
4559
4560
4561
4562
4563
4564
4565
4566
4567
4568
4569
4570
4571
4572
4573
4574
4575
4576
4577
4578
4579
4580
4581
4582
4583
4584
4585
4586
4587
4588
4589
4590
4591
4592
4593
4594
4595
4596
4597
4598
4599
4600
4601
4602
4603
4604
4605
4606
4607
4608
4609
4610
4611
4612
4613
4614
4615
4616
4617
4618
4619
4620
4621
4622
4623
4624
4625
4626
4627
4628
4629
4630
4631
4632
4633
4634
4635
4636
4637
4638
4639
4640
4641
4642
4643
4644
4645
4646
4647
4648
4649
4650
4651
4652
4653
4654
4655
4656
4657
4658
4659
4660
4661
4662
4663
4664
4665
4666
4667
4668
4669
4670
4671
4672
4673
4674
4675
4676
4677
4678
4679
4680
4681
4682
4683
4684
4685
4686
4687
4688
4689
4690
4691
4692
4693
4694
4695
4696
4697
4698
4699
4700
4701
4702
4703
4704
4705
4706
4707
4708
4709
4710
4711
4712
4713
4714
4715
4716
4717
4718
4719
4720
4721
4722
4723
4724
4725
4726
4727
4728
4729
4730
4731
4732
4733
4734
4735
4736
4737
4738
4739
4740
4741
4742
4743
4744
4745
4746
4747
4748
4749
4750
4751
4752
4753
4754
4755
4756
4757
4758
4759
4760
4761
4762
4763
4764
4765
4766
4767
4768
4769
4770
4771
4772
4773
4774
4775
4776
4777
4778
4779
4780
4781
4782
4783
4784
4785
4786
4787
4788
4789
4790
4791
4792
4793
4794
4795
4796
4797
4798
4799
4800
4801
4802
4803
4804
4805
4806
4807
4808
4809
4810
4811
4812
4813
4814
4815
4816
4817
4818
4819
4820
4821
4822
4823
4824
4825
4826
4827
4828
4829
4830
4831
4832
4833
4834
4835
4836
4837
4838
4839
4840
4841
4842
4843
4844
4845
4846
4847
4848
4849
4850
4851
4852
4853
4854
4855
4856
4857
4858
4859
4860
4861
4862
4863
4864
4865
4866
4867
4868
4869
4870
4871
4872
4873
4874
4875
4876
4877
4878
4879
4880
4881
4882
4883
4884
4885
4886
4887
4888
4889
4890
4891
4892
4893
4894
4895
4896
4897
4898
4899
4900
4901
4902
4903
4904
4905
4906
4907
4908
4909
4910
4911
4912
4913
4914
4915
4916
4917
4918
4919
4920
4921
4922
4923
4924
4925
4926
4927
4928
4929
4930
4931
4932
4933
4934
4935
4936
4937
4938
4939
4940
4941
4942
4943
4944
4945
4946
4947
4948
4949
4950
4951
4952
4953
4954
4955
4956
4957
4958
4959
4960
4961
4962
4963
4964
4965
4966
4967
4968
4969
4970
4971
4972
4973
4974
4975
4976
4977
4978
4979
4980
4981
4982
4983
4984
4985
4986
4987
4988
4989
4990
4991
4992
4993
4994
4995
4996
4997
4998
4999
5000
5001
5002
5003
5004
5005
5006
5007
5008
5009
5010
5011
5012
5013
5014
5015
5016
5017
5018
5019
5020
5021
5022
5023
5024
5025
5026
5027
5028
5029
5030
5031
5032
5033
5034
5035
5036
5037
5038
5039
5040
5041
5042
5043
5044
5045
5046
5047
5048
5049
5050
5051
5052
5053
5054
5055
5056
5057
5058
5059
5060
5061
5062
5063
5064
5065
5066
5067
5068
5069
5070
5071
5072
def validateX13spec(spec: X13spec) -> None:  # noqa: N802, PLR0912, PLR0915
    """Cross-spec invariant check — runs before the binary sees the spec.

    Mirrors ``x13spec.jl:3563-4055`` (``validateX13spec``). Branches that
    Julia handles with ``ArgumentError`` raise :exc:`ValueError`; branches
    that Julia logs via ``@warn`` emit :class:`UserWarning` through
    :func:`warnings.warn` (the project's ``error::UserWarning`` filter at
    `pyproject.toml` turns silent misuse into a test-failure, mirroring
    the Julia upstream's `@test_warn` discipline).

    Coverage spans:

    * ARIMA-source mutual exclusion: ``arima`` ⊥ ``automdl`` ⊥ ``pickmdl``.
    * ``estimate.file`` overrides: blocks ``arima.{model,ar,ma}`` and
      ``regression.{variables,user,b}``.
    * Forecast / history overlap: ``history.fstep ≤ forecast.maxlead``.
    * Regression variable type compatibility (~16 raise sites): the
      trading-day / leap-year / length-of-period / stock-vs-flow
      compatibility matrix per ``x13spec.jl:3621-3777``.
    * AIC-test type compatibility (~12 raise sites): parallel to the
      variables matrix but for the ``aictest=`` argument.
    * Outlier-range containment: ``ao`` / ``ls`` / ``so`` / ``tc`` / range
      variants must lie within the series range.
    * Regression / x11regression data-range containment (with forecast
      backcast/maxlead expansion).
    * Slidingspans length bounds (quarterly: 12 ≤ length ≤ 76; monthly:
      36 ≤ length ≤ 228).
    * Seats HP-filter sample-size warning.
    * Modelspan-vs-span backcast warning.
    * Transform/x11 mode compatibility (``adjust`` vs ``x11.mode``).
    * X11regression-vs-forcecal warning.
    """
    if not isinstance(spec.series, X13series):
        msg = (
            "X13spec.series must be set to an X13series instance before "
            "validateX13spec is called. Build the spec via "
            "tsecon.x13.newspec(series_or_tseries, ...)."
        )
        raise ValueError(msg)
    s = spec.series

    # --- arima ⊥ automdl, arima ⊥ pickmdl, automdl ⊥ pickmdl --------------
    if not isinstance(spec.arima, X13default):
        if not isinstance(spec.automdl, X13default):
            msg = (
                "The arima spec cannot be used in the same spec file "
                "as the pickmdl or automdl specs."
            )
            raise ValueError(msg)
        if not isinstance(spec.pickmdl, X13default):
            msg = (
                "The arima spec cannot be used in the same spec file "
                "as the pickmdl or automdl specs."
            )
            raise ValueError(msg)
        # estimate.file ⊥ arima.{model,ar,ma}
        if (
            not isinstance(spec.estimate, X13default)
            and not isinstance(spec.estimate.file, X13default)
            and (
                not isinstance(spec.arima.ar, X13default)
                or not isinstance(spec.arima.ma, X13default)
                or not isinstance(spec.arima.model, X13default)
            )
        ):
            msg = (
                "The model, ma, and ar arguments of the arima spec cannot "
                "be used when the file argument is specified in the "
                "estimate spec."
            )
            raise ValueError(msg)

    if not isinstance(spec.automdl, X13default):
        if not isinstance(spec.pickmdl, X13default):
            msg = (
                "The automdl spec cannot be used in the same spec file "
                "as the pickmdl or arima specs."
            )
            raise ValueError(msg)
        if not isinstance(spec.estimate, X13default) and not isinstance(
            spec.estimate.file, X13default
        ):
            msg = (
                "The automdl spec cannot be used in the same spec file "
                "as an estimate spec employing the file argument."
            )
            raise ValueError(msg)

    # --- estimate.file ⊥ regression.{variables, user, b} ------------------
    if (
        not isinstance(spec.estimate, X13default)
        and not isinstance(spec.estimate.file, X13default)
        and not isinstance(spec.regression, X13default)
        and (
            not isinstance(spec.regression.variables, X13default)
            or not isinstance(spec.regression.user, X13default)
            or not isinstance(spec.regression.b, X13default)
        )
    ):
        msg = (
            "The variables, user, and b arguments of the regression spec "
            "cannot be used when the estimate spec contains the file argument."
        )
        raise ValueError(msg)

    # --- history.fstep vs forecast.maxlead --------------------------------
    if (
        not isinstance(spec.forecast, X13default)
        and not isinstance(spec.forecast.maxlead, X13default)
        and not isinstance(spec.history, X13default)
        and not isinstance(spec.history.fstep, X13default)
    ):
        maxlead = spec.forecast.maxlead
        fstep = spec.history.fstep
        if isinstance(fstep, list) and any(f > maxlead for f in fstep):
            msg = (
                f"The values of fstep in the history spec cannot be "
                f"greater than the maxlead specified in the forecast "
                f"spec ({maxlead}). Received: {fstep}."
            )
            raise ValueError(msg)
        if not isinstance(fstep, list) and fstep > maxlead:
            msg = (
                f"The values of fstep in the history spec cannot be "
                f"greater than the maxlead specified in the forecast "
                f"spec ({maxlead}). Received: {fstep}."
            )
            raise ValueError(msg)

    # --- history.outlier without an outlier spec --------------------------
    if (
        not isinstance(spec.history, X13default)
        and not isinstance(spec.history.outlier, X13default)
        and isinstance(spec.outlier, X13default)
    ):
        warnings.warn(
            "The outlier argument of the history spec has no effect "
            "when no outlier spec is specified.",
            UserWarning,
            stacklevel=2,
        )

    # --- regression-variable type compatibility ---------------------------
    if not isinstance(spec.regression, X13default) and not isinstance(
        spec.regression.variables, X13default
    ):
        vars_list = _vars_as_list(spec.regression.variables)
        if (
            not isinstance(spec.transform, X13default)
            and not isinstance(spec.transform.adjust, X13default)
            and spec.transform.adjust == "lom"
        ):
            for v in vars_list:
                tok = _resolve_var_token(v)
                if tok in ("td", "lom"):
                    msg = (
                        "When adjust='lom' is specified in the transform "
                        "spec, the inclusion of either td or lom variables "
                        "in the variables list of the regression spec "
                        "leads to conflicts."
                    )
                    raise ValueError(msg)

        types_used = _collect_regvar_types(vars_list)
        series_range = s.data.range
        span_range = _effective_span(s)
        for v in vars_list:
            tok = _resolve_var_token(v)
            # type-vs-series-type guard
            if not isinstance(s.type, X13default):
                if s.type != "flow":
                    if tok in (
                        "td",
                        "tdnolpyear",
                        "td1coef",
                        "td1nolpyear",
                        "lpyear",
                        "easter",
                        "labor",
                        "thank",
                        "sceaster",
                    ):
                        msg = (
                            f"{tok} regressors can only be used with "
                            f"flow-type data. The provided series has the "
                            f"type: {s.type}."
                        )
                        raise ValueError(msg)
                elif s.type != "stock" and tok in ("tdstock", "td1stock", "easterstock"):
                    msg = (
                        f"{tok} regressors can only be used with "
                        f"stock-type data. The provided series has the "
                        f"type: {s.type}."
                    )
                    raise ValueError(msg)

            # td / tdnolpyear / td1coef / td1nolpyear / lpyear / lom / loq
            if tok == "td":
                if not is_monthly(s.data.frequency) and not is_quarterly(s.data.frequency):
                    msg = "td regressors can only be used with Monthly or Quarterly data."
                    raise ValueError(msg)
                if types_used & {
                    "tdnolpyear",
                    "td1coef",
                    "td1nolpyear",
                    "lpyear",
                    "lom",
                    "loq",
                    "tdstock",
                    "tdstock1coef",
                }:
                    msg = (
                        "td cannot be used with tdnolpyear, td1coef, "
                        "td1nolpyear, lpyear, lom, loq, tdstock, or "
                        "tdstock1coef regressors."
                    )
                    raise ValueError(msg)
                if not isinstance(spec.transform, X13default) and not isinstance(
                    spec.transform.adjust, X13default
                ):
                    msg = (
                        "The adjust argument of the transform spec cannot "
                        "be used when td or td1coef is specified in the "
                        "regression spec."
                    )
                    raise ValueError(msg)
            elif tok == "tdnolpyear":
                if not is_monthly(s.data.frequency) and not is_quarterly(s.data.frequency):
                    msg = "tdnolpyear regressors can only be used with Monthly or Quarterly data."
                    raise ValueError(msg)
                if types_used & {"td", "td1coef", "td1nolpyear", "tdstock", "tdstock1coef"}:
                    msg = (
                        "tdnolpyear cannot be used with td, td1coef, "
                        "td1nolpyear, tdstock, or tdstock1coef regressors."
                    )
                    raise ValueError(msg)
            elif tok == "td1coef":
                if not is_monthly(s.data.frequency) and not is_quarterly(s.data.frequency):
                    msg = "td1coef regressors can only be used with Monthly or Quarterly data."
                    raise ValueError(msg)
                if types_used & {
                    "td",
                    "tdnolpyear",
                    "td1nolpyear",
                    "lpyear",
                    "lom",
                    "loq",
                    "tdstock",
                    "tdstock1coef",
                }:
                    msg = (
                        "td1coef cannot be used with td, tdnolpyear, "
                        "td1nolpyear, lpyear, lom, loq, tdstock, or "
                        "tdstock1coef regressors."
                    )
                    raise ValueError(msg)
                if not isinstance(spec.transform, X13default) and not isinstance(
                    spec.transform.adjust, X13default
                ):
                    msg = (
                        "The adjust argument of the transform spec cannot "
                        "be used when td or td1coef is specified in the "
                        "regression spec."
                    )
                    raise ValueError(msg)
            elif tok == "td1nolpyear":
                if not is_monthly(s.data.frequency) and not is_quarterly(s.data.frequency):
                    msg = "td1nolpyear regressors can only be used with Monthly or Quarterly data."
                    raise ValueError(msg)
                if types_used & {"td", "tdnolpyear", "td1coef", "tdstock", "tdstock1coef"}:
                    msg = (
                        "td1nolpyear cannot be used with td, tdnolpyear, "
                        "td1coef, tdstock, or tdstock1coef regressors."
                    )
                    raise ValueError(msg)
            elif tok == "lpyear":
                if not is_monthly(s.data.frequency) and not is_quarterly(s.data.frequency):
                    msg = "lpyear regressors can only be used with Monthly or Quarterly data."
                    raise ValueError(msg)
                if types_used & {"td", "td1coef", "tdstock", "tdstock1coef"}:
                    msg = (
                        "lpyear cannot be used with td, td1coef, tdstock, "
                        "or tdstock1coef regressors."
                    )
                    raise ValueError(msg)
            elif tok == "lom":
                if not is_monthly(s.data.frequency) and not is_quarterly(s.data.frequency):
                    msg = "lom regressors can only be used with Monthly or Quarterly data."
                    raise ValueError(msg)
                if types_used & {"td", "td1coef", "tdstock", "tdstock1coef"}:
                    msg = (
                        "lom cannot be used with td, td1coef, tdstock, or tdstock1coef regressors."
                    )
                    raise ValueError(msg)
            elif tok == "loq":
                if not is_monthly(s.data.frequency) and not is_quarterly(s.data.frequency):
                    msg = "loq regressors can only be used with Monthly or Quarterly data."
                    raise ValueError(msg)
                if types_used & {"td", "td1coef", "tdstock", "tdstock1coef"}:
                    msg = (
                        "loq cannot be used with td, td1coef, tdstock, or tdstock1coef regressors."
                    )
                    raise ValueError(msg)
            elif tok == "tdstock":
                if not is_monthly(s.data.frequency):
                    msg = "tdstock regressors can only be used with Monthly data."
                    raise ValueError(msg)
                if types_used & {
                    "tdstock1coef",
                    "td",
                    "tdnolpyear",
                    "td1coef",
                    "td1nolpyear",
                    "lom",
                    "loq",
                }:
                    msg = (
                        "tdstock cannot be used with tdstock1coef, td, "
                        "tdnolpyear, td1coef, td1nolpyear, lom or loq "
                        "regressors."
                    )
                    raise ValueError(msg)
            elif tok == "tdstock1coef":
                if not is_monthly(s.data.frequency):
                    msg = "tdstock1coef regressors can only be used with Monthly data."
                    raise ValueError(msg)
                if types_used & {
                    "tdstock",
                    "td",
                    "tdnolpyear",
                    "td1coef",
                    "td1nolpyear",
                    "lom",
                    "loq",
                }:
                    msg = (
                        "tdstock1coef cannot be used with tdstock, td, "
                        "tdnolpyear, td1coef, td1nolpyear, lom or loq "
                        "regressors."
                    )
                    raise ValueError(msg)
            elif tok == "labor":
                if not is_monthly(s.data.frequency):
                    msg = "labor regressors can only be used with Monthly data."
                    raise ValueError(msg)
            elif tok == "sceaster":
                if not is_monthly(s.data.frequency) and not is_quarterly(s.data.frequency):
                    msg = "sceaster regressors can only be used with Monthly data."
                    raise ValueError(msg)

            # range / point containment for the outlier subset
            if isinstance(v, ao) and v.mit not in series_range:
                msg = (
                    f"ao regressors must have a date within the series range "
                    f"({series_range!r}). Received: ao({v.mit!r})"
                )
                raise ValueError(msg)
            if isinstance(v, tc) and v.mit not in series_range:
                msg = (
                    f"tc regressors must have a date within the series range "
                    f"({series_range!r}). Received: tc({v.mit!r})"
                )
                raise ValueError(msg)
            if isinstance(v, ls):
                if v.mit not in series_range:
                    msg = (
                        f"ls regressors must have a date within the series "
                        f"range ({series_range!r}). Received: ls({v.mit!r})"
                    )
                    raise ValueError(msg)
                if v.mit == span_range.first():
                    msg = (
                        f"ls regressors cannot be at the start of the series "
                        f"range or the span range ({span_range!r}). Received: "
                        f"ls({v.mit!r})"
                    )
                    raise ValueError(msg)
            if isinstance(v, so):
                if v.mit not in series_range:
                    msg = (
                        f"so regressors must have a date within the series "
                        f"range ({series_range!r}). Received: so({v.mit!r})"
                    )
                    raise ValueError(msg)
                if v.mit == span_range.first():
                    msg = (
                        f"so regressors cannot be at the start of the series "
                        f"range or the span range ({span_range!r}). Received: "
                        f"so({v.mit!r})"
                    )
                    raise ValueError(msg)
            for cls, name in (
                (aos, "aos"),
                (lss, "lss"),
                (rp, "rp"),
                (qd, "qd"),
                (qi, "qi"),
                (tl, "tl"),
            ):
                if isinstance(v, cls) and (
                    v.mit1 not in series_range or v.mit2 not in series_range
                ):
                    msg = (
                        f"{name} regressors must have a date within the "
                        f"series range ({series_range!r}). Received: "
                        f"{name}({v.mit1!r},{v.mit2!r})"
                    )
                    raise ValueError(msg)

        # AIC-test compatibility (parallel to the variables matrix above)
        if not isinstance(spec.regression.aictest, X13default):
            aictests = (
                [spec.regression.aictest]
                if isinstance(spec.regression.aictest, str)
                else list(spec.regression.aictest)
            )
            for aic in aictests:
                if not isinstance(s.type, X13default):
                    if s.type != "flow":
                        if aic in (
                            "tdnolpyear",
                            "td1coef",
                            "td1nolpyear",
                            "lpyear",
                            "easter",
                            "labor",
                            "thank",
                            "sceaster",
                        ):
                            msg = (
                                f"aictest: {aic} regressors can only be "
                                f"tested for with flow-type data. The "
                                f"provided series has the type: {s.type}."
                            )
                            raise ValueError(msg)
                    elif s.type != "stock" and aic in ("tdstock", "td1stock", "easterstock"):
                        msg = (
                            f"aictest: {aic} regressors can only be "
                            f"tested for with stock-type data. The "
                            f"provided series has the type: {s.type}."
                        )
                        raise ValueError(msg)

                if aic == "td":
                    if not is_monthly(s.data.frequency) and not is_quarterly(s.data.frequency):
                        msg = (
                            "aictest: td regressors can only be used with "
                            "Monthly or Quarterly data."
                        )
                        raise ValueError(msg)
                    if types_used & {"lpyear", "lom", "loq"}:
                        msg = "aictest: td cannot be used with lpyear, lom, loq, regressors."
                        raise ValueError(msg)
                elif aic == "tdnolpyear":
                    if not is_monthly(s.data.frequency) and not is_quarterly(s.data.frequency):
                        msg = (
                            "aictest: tdnolpyear regressors can only be "
                            "used with Monthly or Quarterly data."
                        )
                        raise ValueError(msg)
                    if types_used & {"td", "td1coef", "td1nolpyear", "tdstock", "tdstock1coef"}:
                        msg = (
                            "aictest: tdnolpyear cannot be used with td, "
                            "td1coef, td1nolpyear, tdstock, or tdstock1coef "
                            "regressors."
                        )
                        raise ValueError(msg)
                elif aic == "td1coef":
                    if not is_monthly(s.data.frequency) and not is_quarterly(s.data.frequency):
                        msg = (
                            "aictest: td1coef regressors can only be used "
                            "with Monthly or Quarterly data."
                        )
                        raise ValueError(msg)
                    if types_used & {
                        "td",
                        "tdnolpyear",
                        "td1nolpyear",
                        "lpyear",
                        "lom",
                        "loq",
                        "tdstock",
                        "tdstock1coef",
                    }:
                        msg = (
                            "aictest: td1coef cannot be used with td, "
                            "tdnolpyear, td1nolpyear, lpyear, lom, loq, "
                            "tdstock, or tdstock1coef regressors."
                        )
                        raise ValueError(msg)
                elif aic == "td1nolpyear":
                    if not is_monthly(s.data.frequency) and not is_quarterly(s.data.frequency):
                        msg = (
                            "aictest: td1nolpyear regressors can only be "
                            "used with Monthly or Quarterly data."
                        )
                        raise ValueError(msg)
                    if types_used & {"td", "tdnolpyear", "td1coef", "tdstock", "tdstock1coef"}:
                        msg = (
                            "aictest: td1nolpyear cannot be used with td, "
                            "tdnolpyear, td1coef, tdstock, or tdstock1coef "
                            "regressors."
                        )
                        raise ValueError(msg)
                elif aic == "lpyear":
                    if not is_monthly(s.data.frequency) and not is_quarterly(s.data.frequency):
                        msg = (
                            "aictest: lpyear regressors can only be used "
                            "with Monthly or Quarterly data."
                        )
                        raise ValueError(msg)
                    if types_used & {"td", "td1coef", "tdstock", "tdstock1coef"}:
                        msg = (
                            "aictest: lpyear cannot be used with td, "
                            "td1coef, tdstock, or tdstock1coef regressors."
                        )
                        raise ValueError(msg)
                elif aic == "lom":
                    if not is_monthly(s.data.frequency):
                        msg = "aictest: lom regressors can only tested for with Monthly data."
                        raise ValueError(msg)
                    if types_used & {"td", "td1coef", "tdstock", "tdstock1coef"}:
                        msg = (
                            "aictest: lom cannot be used with td, td1coef, "
                            "tdstock, or tdstock1coef regressors."
                        )
                        raise ValueError(msg)
                elif aic == "loq":
                    if not is_quarterly(s.data.frequency):
                        msg = "aictest: loq regressors can only be tested for with Quarterly data."
                        raise ValueError(msg)
                    if types_used & {"td", "td1coef", "tdstock", "tdstock1coef"}:
                        msg = (
                            "aictest: loq cannot be used with td, td1coef, "
                            "tdstock, or tdstock1coef regressors."
                        )
                        raise ValueError(msg)
                elif aic == "tdstock":
                    if not is_monthly(s.data.frequency):
                        msg = "aictest: tdstock regressors can only be used with Monthly data."
                        raise ValueError(msg)
                    if types_used & {
                        "tdstock1coef",
                        "td",
                        "tdnolpyear",
                        "td1coef",
                        "td1nolpyear",
                        "lom",
                        "loq",
                    }:
                        msg = (
                            "aictest: tdstock cannot be used with "
                            "tdstock1coef, td, tdnolpyear, td1coef, "
                            "td1nolpyear, lom or loq regressors."
                        )
                        raise ValueError(msg)
                elif aic == "tdstock1coef":
                    if not is_monthly(s.data.frequency):
                        msg = "aictest: tdstock1coef regressors can only be used with Monthly data."
                        raise ValueError(msg)
                    if types_used & {
                        "tdstock",
                        "td",
                        "tdnolpyear",
                        "td1coef",
                        "td1nolpyear",
                        "lom",
                        "loq",
                    }:
                        msg = (
                            "aictest: tdstock1coef cannot be used with "
                            "tdstock, td, tdnolpyear, td1coef, "
                            "td1nolpyear, lom or loq regressors."
                        )
                        raise ValueError(msg)
                elif aic == "labor":
                    if not is_monthly(s.data.frequency):
                        msg = "aictest: labor regressors can only be used with Monthly data."
                        raise ValueError(msg)
                elif aic == "sceaster":
                    if not is_monthly(s.data.frequency) and not is_quarterly(s.data.frequency):
                        msg = "aictest: sceaster regressors can only be used with Monthly data."
                        raise ValueError(msg)

    # --- regression.data range containment --------------------------------
    if not isinstance(spec.regression, X13default) and not isinstance(
        spec.regression.data, X13default
    ):
        data_range = spec.regression.data.range
        required_range = _effective_span(s)
        if not isinstance(spec.forecast, X13default):
            first_ = required_range.first()
            last_ = required_range.last()
            if not isinstance(spec.forecast.maxback, X13default):
                first_ = MIT(first_.frequency, first_.value - spec.forecast.maxback)
            if not isinstance(spec.forecast.maxlead, X13default):
                last_ = MIT(last_.frequency, last_.value + spec.forecast.maxlead)
            required_range = MITRange(first_, last_)
        if not _mit_range_contains_range(data_range, required_range):
            msg = (
                f"The data provided in the regression spec must cover the "
                f"range of the supplied data (or the span specified by the "
                f"span argument of the series spec), as well as any "
                f"forecasts and backcasts requested by the forecast spec. "
                f"The required range is {required_range!r}, but the "
                f"provided range was only {data_range!r}."
            )
            raise ValueError(msg)

    # --- seats hpcycle small-sample warning -------------------------------
    if (
        not isinstance(spec.seats, X13default)
        and not isinstance(spec.seats.hpcycle, X13default)
        and isinstance(spec.seats.hplan, X13default)
        and spec.seats.hpcycle is True
    ):
        n = len(s.data)
        if is_monthly(s.data.frequency) and n < 120:
            warnings.warn(
                f"Hodrick-Prescott filters will not be used as the "
                f"default hplan requires at least 120 monthly "
                f"observations. The provided series has {n} observations.",
                UserWarning,
                stacklevel=2,
            )
        elif is_quarterly(s.data.frequency) and n < 48:
            warnings.warn(
                f"Hodrick-Prescott filters will not be used as the "
                f"default hplan requires at least 48 quarterly "
                f"observations. The provided series has {n} observations.",
                UserWarning,
                stacklevel=2,
            )

    # --- modelspan / span backcast warning --------------------------------
    if (
        not isinstance(s.modelspan, X13default)
        and not isinstance(spec.forecast, X13default)
        and not isinstance(spec.forecast.maxback, X13default)
        and not isinstance(s.span, X13default)
    ):
        # The outer guard ruled out X13default for both; narrow accordingly.
        # Span is the only remaining shape after the X13default outer guard
        # when the value is not an MITRange.
        modelspan_first: MIT | None = (
            s.modelspan.first() if isinstance(s.modelspan, MITRange) else s.modelspan.b
        )
        span_first: MIT | None = s.span.first() if isinstance(s.span, MITRange) else s.span.b
        if modelspan_first is not None and span_first is not None and modelspan_first != span_first:
            warnings.warn(
                f"Backcasts will not be generated as the start of the "
                f"modelspan specified ({s.modelspan!r}) does not coincide "
                f"with the start of the series span specified ({s.span!r}).",
                UserWarning,
                stacklevel=2,
            )

    # --- slidingspans length bounds + outlier-without-spec warn -----------
    if not isinstance(spec.slidingspans, X13default):
        ss = spec.slidingspans
        if not isinstance(ss.length, X13default):
            if is_quarterly(s.data.frequency):
                if ss.length < 12:
                    msg = (
                        f"The length argument of the slidingspans spec "
                        f"must cover at least 3 years. Current length is "
                        f"≈{ss.length / 4} years."
                    )
                    raise ValueError(msg)
                if ss.length > 4 * 19:
                    msg = (
                        f"The length argument of the slidingspans spec "
                        f"can cover at most 19 years. Current length is "
                        f"≈{ss.length / 4} years."
                    )
                    raise ValueError(msg)
            if is_monthly(s.data.frequency):
                if ss.length < 36:
                    msg = (
                        f"The length argument of the slidingspans spec "
                        f"must cover at least 3 years. Current length is "
                        f"≈{ss.length / 12} years."
                    )
                    raise ValueError(msg)
                if ss.length > 12 * 19:
                    msg = (
                        f"The length argument of the slidingspans spec "
                        f"can cover at most 19 years. Current length is "
                        f"≈{ss.length / 12} years."
                    )
                    raise ValueError(msg)
        if not isinstance(ss.outlier, X13default) and isinstance(spec.outlier, X13default):
            warnings.warn(
                "The outlier argument of the slidingspans spec will be "
                "ignored as there is no outlier spec specified.",
                UserWarning,
                stacklevel=2,
            )

    # --- spectrum.qcheck on non-monthly warn ------------------------------
    if (
        not isinstance(spec.spectrum, X13default)
        and not isinstance(spec.spectrum.qcheck, X13default)
        and spec.spectrum.qcheck is True
        and not is_monthly(s.data.frequency)
    ):
        warnings.warn(
            "The qcheck argument of the spectrum spec only produces output for a monthly TSeries.",
            UserWarning,
            stacklevel=2,
        )

    # --- transform.adjust ⊥ x11.mode ∈ {add, pseudoadd} -------------------
    if not isinstance(spec.transform, X13default):
        if (
            not isinstance(spec.transform.adjust, X13default)
            and not isinstance(spec.x11, X13default)
            and not isinstance(spec.x11.mode, X13default)
            and spec.x11.mode in ("add", "pseudoadd")
        ):
            msg = (
                "The adjust argument of the transform spec cannot be "
                "used when the mode argument of the x11 spec is 'add' "
                "or 'pseudoadd'."
            )
            raise ValueError(msg)
        if (
            not isinstance(spec.x11, X13default)
            and isinstance(spec.x11.mode, X13default)
            and isinstance(spec.transform.power, X13default)
            and isinstance(spec.transform.func, X13default)
        ):
            msg = (
                "The default value for the mode argument of the x11 "
                "spec (multiplicative) conflicts with the default for "
                "the function and power arguments of the transform "
                "spec (no transformation)."
            )
            raise ValueError(msg)

    # --- x11regression: data / umdata / outlierspan / span containment ----
    if not isinstance(spec.x11regression, X13default):
        x11r = spec.x11regression
        if not isinstance(x11r.data, X13default):
            data_range = x11r.data.range
            required_range = _effective_span(s)
            if not isinstance(spec.forecast, X13default):
                first_ = required_range.first()
                last_ = required_range.last()
                if not isinstance(spec.forecast.maxback, X13default):
                    first_ = MIT(first_.frequency, first_.value - spec.forecast.maxback)
                if not isinstance(spec.forecast.maxlead, X13default):
                    last_ = MIT(last_.frequency, last_.value + spec.forecast.maxlead)
                required_range = MITRange(first_, last_)
            if not _mit_range_contains_range(data_range, required_range):
                msg = (
                    f"The data provided in the x11regression spec must "
                    f"cover the range of the supplied data (or the span "
                    f"specified by the span argument of the series spec), "
                    f"as well as any forecasts and backcasts requested by "
                    f"the forecast spec. The required range is "
                    f"{required_range!r}, but the provided range was only "
                    f"{data_range!r}."
                )
                raise ValueError(msg)
        if not isinstance(x11r.umdata, X13default):
            data_range = x11r.umdata.range
            required_range = _effective_span(s)
            if not isinstance(spec.forecast, X13default):
                first_ = required_range.first()
                last_ = required_range.last()
                if not isinstance(spec.forecast.maxback, X13default):
                    first_ = MIT(first_.frequency, first_.value - spec.forecast.maxback)
                if not isinstance(spec.forecast.maxlead, X13default):
                    last_ = MIT(last_.frequency, last_.value + spec.forecast.maxlead)
                required_range = MITRange(first_, last_)
            if not _mit_range_contains_range(data_range, required_range):
                msg = (
                    f"The umdata provided in the x11regression spec must "
                    f"cover the range of the supplied data (or the span "
                    f"specified by the span argument of the series spec), "
                    f"as well as any forecasts and backcasts requested by "
                    f"the forecast spec. The required range is "
                    f"{required_range!r}, but the provided range was only "
                    f"{data_range!r}."
                )
                raise ValueError(msg)
        if (
            not isinstance(x11r.outlierspan, X13default)
            and isinstance(x11r.outlierspan, MITRange)
            and not _mit_range_contains_range(s.data.range, x11r.outlierspan)
        ):
            msg = (
                f"The outlierspan argument of the x11regression spec "
                f"must lie within the range of the provided data "
                f"({s.data.range!r}). Received: {x11r.outlierspan!r}."
            )
            raise ValueError(msg)
        if not isinstance(x11r.span, X13default):
            required_range = _effective_span(s)
            reg_range = required_range
            if isinstance(x11r.span, MITRange):
                reg_range = x11r.span
            elif isinstance(x11r.span, Span):
                if x11r.span.b is not None:
                    reg_range = MITRange(x11r.span.b, required_range.last())
                if x11r.span.e is not None:
                    reg_range = MITRange(reg_range.first(), x11r.span.e)
            if not _mit_range_contains_range(required_range, reg_range):
                msg = (
                    f"The span argument of the x11regression spec must "
                    f"lie within the range of the provided data "
                    f"({required_range!r}). Received: {reg_range!r}."
                )
                raise ValueError(msg)
        if not isinstance(x11r.variables, X13default):
            vars_list = _vars_as_list(x11r.variables)
            x11r_types_used = _collect_regvar_types(vars_list)
            if not isinstance(x11r.usertype, X13default):
                if isinstance(x11r.usertype, str):
                    x11r_types_used.add(x11r.usertype)
                else:
                    x11r_types_used.update(x11r.usertype)
            if not isinstance(x11r.forcecal, X13default):
                td_types = {"td", "td1coef", "tdstock", "tdstock1coef"}
                holiday_types = {"easter", "labor", "thank", "sceaster"}
                if not (x11r_types_used & td_types and x11r_types_used & holiday_types):
                    warnings.warn(
                        "The forcecal argument of the x11regression will "
                        "not have any effect as the variables argument "
                        "does not contain both td and holiday regressors.",
                        UserWarning,
                        stacklevel=2,
                    )

Writer

tsecon.x13._write

X-13ARIMA-SEATS spec serializer: :class:X13spec -> .spc text.

Mirrors TimeSeriesEcon.jl/src/x13/x13write.jl (294 LOC). Lands in M2.4 alongside :func:tsecon.x13._spec.validateX13spec.

Surface:

  • :func:x13write (x13write.jl:50) — top-level emitter that walks the :class:~tsecon.x13._spec.X13spec and emits each populated sub-spec's text block.
  • :func:impose_line_length (x13write.jl:8) — line-wrap helper that splits long argument lines so the X-13as binary does not reject the spec on its 132-column line limit. Mutates the input list in place (mirrors Julia's impose_line_length! !-suffixed name).
  • :func:emit_block (private) — render one <name> { ... } block from a populated sub-spec dataclass; the writer's main per-block worker.

The Julia version writes a spec.spc text file directly to disk; the Python port returns the text as a :class:str so that callers that do not need the file (testing, dry-run validation) skip the I/O. The X13.run entry (M2.5) calls :func:x13write then writes the returned text to <tempdir>/spec.spc itself, mirroring Julia's open(...) do f; println(f, spec.string); end block.

Line-wrap quirks (mirrors Julia x13write.jl:8-47):

  • _text_len(s) treats each \t as eight columns (Julia _length); the writer never emits tabs, but the wrapper handles them for round-trip fidelity with hand-edited spec strings.
  • " + " is preferred as a split point over plain spaces because print=(table1 + table2 + …) lists are the most common long-argument shape (the Julia upstream's x13write_plus(::Vector{Symbol}) produces them).
  • When the leading whitespace-only fragment of a continuation line cannot fit a single un-splittable token, raise :exc:ValueError rather than recurse infinitely (Julia ArgumentError).

impose_line_length

impose_line_length(
    s: list[str],
    limit: int = _DEFAULT_LINE_LIMIT,
    delve: bool = True,
) -> None

Wrap any over-limit lines in s in place.

Mirrors Julia impose_line_length! (x13write.jl:8-47). Walks the list; for each line longer than limit columns, splits at the last " + " (when present) or otherwise the last space that keeps the prefix within budget, then inserts the suffix — prefixed with 8 spaces of continuation indent — at the next position. Recurses into "\n"-joined sub-lines when delve=True (so embedded newlines, e.g. inside metadata blocks, get wrapped on their own terms before being rejoined).

Raises :exc:ValueError when a continuation line cannot be split further (single un-splittable token over budget) — surfaces the Julia ArgumentError from x13write.jl:38-41.

Source code in src/tsecon/x13/_write.py
def impose_line_length(
    s: list[str],
    limit: int = _DEFAULT_LINE_LIMIT,
    delve: bool = True,
) -> None:
    r"""Wrap any over-limit lines in ``s`` in place.

    Mirrors Julia ``impose_line_length!`` (``x13write.jl:8-47``). Walks
    the list; for each line longer than ``limit`` columns, splits at
    the last ``" + "`` (when present) or otherwise the last space that
    keeps the prefix within budget, then inserts the suffix — prefixed
    with 8 spaces of continuation indent — at the next position.
    Recurses into ``"\n"``-joined sub-lines when ``delve=True`` (so
    embedded newlines, e.g. inside ``metadata`` blocks, get wrapped on
    their own terms before being rejoined).

    Raises :exc:`ValueError` when a continuation line cannot be split
    further (single un-splittable token over budget) — surfaces the
    Julia ``ArgumentError`` from ``x13write.jl:38-41``.
    """
    counter = 0
    while counter < len(s):
        line = s[counter]
        if delve and "\n" in line:
            sub_lines = line.split("\n")
            impose_line_length(sub_lines, limit, False)
            s[counter] = "\n".join(sub_lines)
            counter += 1
            continue
        if _text_len(line) <= limit:
            counter += 1
            continue

        splitchar = " + " if " + " in line else " "
        parts = line.split(splitchar)
        # Find the largest prefix that fits the budget. Julia computes a
        # running cumulative length and stops the first time it exceeds
        # the limit, then keeps everything up to ``i - 1`` on the
        # current line. The ``best_split_index == 0`` case is left
        # alone deliberately — Julia recurses through it until the
        # next-iteration "continuation is 8 whitespace columns wide"
        # guard fires and raises (see below).
        cum = 0
        best_split_index = 2
        for i, part in enumerate(parts, start=1):
            cum += _text_len(part) + _text_len(splitchar)
            if cum > limit:
                best_split_index = i - 1
                break

        s1 = splitchar.join(parts[:best_split_index]) + splitchar
        s2 = _CONTINUATION_INDENT + splitchar.join(parts[best_split_index:])
        if len(s1) == 8 and s1.strip() == "" and len(s2) > limit:
            msg = (
                f"Could not split the following line into components "
                f"shorter than {limit}. Please shorten the argument "
                f"length:\n{s2}"
            )
            raise ValueError(msg)
        s[counter] = s1
        s.insert(counter + 1, s2)
        counter += 1

x13write

x13write(
    spec: X13spec,
    *,
    test: bool = False,
    outfolder: str | None = None,
) -> str

Serialize an :class:X13spec to .spc text.

Mirrors Julia x13write(::X13spec) (x13write.jl:50-80). Runs :func:validateX13spec first; on success, walks series followed by each populated sub-spec field in declaration order, emits each block, and joins them with newlines.

test=True skips the print / save / savelog fields on each block — useful for round-trip and validation tests that don't want to lock the per-binary output defaults.

The rendered text is stored on :attr:X13spec.string as a side-effect (mirrors Julia spec.string = join(s, "\n")); the function additionally returns the text so callers can pipe it to a file or to the binary directly.

outfolder is the directory the M2.5 binary runner created; used by the pickmdl block to emit a side-file (pickmdl.mdl) rather than inline the model list. If :data:None, the model list is inlined.

Source code in src/tsecon/x13/_write.py
def x13write(
    spec: X13spec,
    *,
    test: bool = False,
    outfolder: str | None = None,
) -> str:
    r"""Serialize an :class:`X13spec` to ``.spc`` text.

    Mirrors Julia ``x13write(::X13spec)`` (``x13write.jl:50-80``). Runs
    :func:`validateX13spec` first; on success, walks ``series`` followed
    by each populated sub-spec field in declaration order, emits each
    block, and joins them with newlines.

    ``test=True`` skips the ``print`` / ``save`` / ``savelog`` fields
    on each block — useful for round-trip and validation tests that
    don't want to lock the per-binary output defaults.

    The rendered text is stored on :attr:`X13spec.string` as a
    side-effect (mirrors Julia ``spec.string = join(s, "\n")``); the
    function additionally returns the text so callers can pipe it to
    a file or to the binary directly.

    ``outfolder`` is the directory the M2.5 binary runner created;
    used by the ``pickmdl`` block to emit a side-file (``pickmdl.mdl``)
    rather than inline the model list. If :data:`None`, the model list
    is inlined.
    """
    validateX13spec(spec)
    if not isinstance(spec.series, X13series):
        msg = "x13write: spec.series must be an X13series; build via newspec(...)."
        raise ValueError(msg)

    blocks: list[str] = [_emit_series_block(spec.series, test=test)]
    for fname in _X13SPEC_SUBSPEC_FIELDS:
        val = getattr(spec, fname)
        if not _is_set(val):
            continue
        if isinstance(val, X13metadata):
            blocks.append(_emit_metadata_block(val))
            continue
        block_name = _SPEC_BLOCK_NAMES.get(type(val))
        if block_name is None:
            msg = f"x13write: no block emitter for sub-spec type {type(val).__name__}."
            raise TypeError(msg)
        blocks.append(_emit_generic_block(val, block_name, test=test, outfolder=outfolder))

    text = "\n".join(blocks)
    spec.string = text
    return text

Per-format readers

The nine x13read_* functions parse each output channel X-13 produces (time series, workspace tables, UDG diagnostics, etc.). End users rarely need them — the eager / lazy materialisation built into X13ResultWorkspace handles dispatch — but the parsers are part of the public surface for the same reason they are in the Julia upstream: roundtripping captured X-13 output against fixtures is a common port-validation workflow.

tsecon.x13._result.x13read_series

x13read_series(
    file: str | PathLike[str], freq: Frequency
) -> TSeries | MVTSeries

Parse an X-13 tabular series output file.

Mirrors Julia x13read_series at x13result.jl:455-493. The first line is a tab-separated header row beginning with a date column; line 2 is dashes; lines 3+ are tab-separated rows where the first column is the period token and the remainder are float values.

Returns:

Type Description
* :class:`MVTSeries` when there are 2+ data columns,
* :class:`TSeries` when there is exactly 1 data column,
* empty :class:`TSeries` over the inferred range when the header

row has no data columns past the date column (mirrors the Julia length(headers) == 0 branch).

Source code in src/tsecon/x13/_result.py
def x13read_series(file: str | os.PathLike[str], freq: Frequency) -> TSeries | MVTSeries:
    """Parse an X-13 tabular series output file.

    Mirrors Julia ``x13read_series`` at ``x13result.jl:455-493``. The
    first line is a tab-separated header row beginning with a date
    column; line 2 is dashes; lines 3+ are tab-separated rows where the
    first column is the period token and the remainder are float
    values.

    Returns
    -------
    * :class:`MVTSeries` when there are 2+ data columns,
    * :class:`TSeries` when there is exactly 1 data column,
    * empty :class:`TSeries` over the inferred range when the header
        row has no data columns past the date column (mirrors the Julia
        ``length(headers) == 0`` branch).
    """
    text = Path(file).read_text(encoding="utf-8", errors="replace")
    lines = text.split("\n")
    if len(lines) < 3:
        msg = f"X-13 series file too short to parse: {file!s} ({len(lines)} lines)."
        raise ValueError(msg)
    headers_tokens = lines[0].split("\t")[1:]
    headers = [_sanitize_colname(h) for h in headers_tokens]
    data_lines = [line for line in lines[2:] if line.strip()]
    if not data_lines:
        msg = f"X-13 series file has no data rows: {file!s}."
        raise ValueError(msg)
    first_row = data_lines[0].split("\t")
    lastcol = len(first_row)
    if lastcol > len(headers) + 1 and not first_row[-1].strip():
        lastcol -= 1
    n = len(data_lines)
    if headers:
        vals = np.empty((n, len(headers)), dtype=np.float64)
        for i, line in enumerate(data_lines):
            parts = line.split("\t")[1:lastcol]
            for j, v in enumerate(parts):
                if j >= len(headers):
                    break
                fv = _tryparse_float(v.strip(), default=float("nan"))
                vals[i, j] = float("nan") if fv is None else fv
    else:
        vals = np.empty((n, 0), dtype=np.float64)
    first_period = first_row[0]
    start = _parse_period_string(first_period, freq)
    if len(headers) > 1:
        return MVTSeries(start, headers, vals)
    if len(headers) == 0:
        end = MIT(start.frequency, start.value + n - 1)
        return TSeries(MITRange(start, end))
    return TSeries(start, vals[:, 0])

tsecon.x13._result.x13read_seatsseries

x13read_seatsseries(
    lines: Sequence[str], freq: Frequency
) -> TSeries | MVTSeries

Parse a SEATS-format series file (whitespace-delimited, indented).

Mirrors Julia x13read_seatsseries at x13result.jl:495-530. SEATS output uses \s\s+ as the column delimiter (two or more consecutive whitespace chars) and offsets the header row by 1-2 lines depending on the layout.

Source code in src/tsecon/x13/_result.py
def x13read_seatsseries(  # noqa: PLR0912 - mirrors Julia's column / header / period branch ladder
    lines: Sequence[str], freq: Frequency
) -> TSeries | MVTSeries:
    r"""Parse a SEATS-format series file (whitespace-delimited, indented).

    Mirrors Julia ``x13read_seatsseries`` at ``x13result.jl:495-530``.
    SEATS output uses ``\s\s+`` as the column delimiter (two or more
    consecutive whitespace chars) and offsets the header row by 1-2
    lines depending on the layout.
    """
    delim = re.compile(r"\s\s+")
    if not lines:
        msg = "X-13 SEATS output is empty."
        raise ValueError(msg)
    headers_line = 1  # 0-indexed; Julia ``headers_line = 2`` is 1-indexed
    if not delim.search(lines[headers_line]):
        headers_line += 1
    if headers_line >= len(lines):
        msg = "X-13 SEATS output has no header row."
        raise ValueError(msg)
    header_parts = delim.split(lines[headers_line])
    headers = [_sanitize_colname(h) for h in header_parts[2:]]
    data_lines = list(lines[headers_line + 1 : -1])
    data_lines = [line for line in data_lines if line.strip()]
    if not data_lines:
        msg = "X-13 SEATS output has no data rows."
        raise ValueError(msg)
    first_row = delim.split(data_lines[0])
    lastcol = len(first_row)
    if lastcol > len(headers) + 1 and not first_row[-1]:
        lastcol -= 1
    n = len(data_lines)
    if headers:
        vals = np.empty((n, len(headers)), dtype=np.float64)
        for i, line in enumerate(data_lines):
            parts = delim.split(line)[1:lastcol]
            for j, v in enumerate(parts):
                if j >= len(headers):
                    break
                fv = _tryparse_float(v.strip(), default=float("nan"))
                vals[i, j] = float("nan") if fv is None else fv
    else:
        vals = np.empty((n, 0), dtype=np.float64)
    # SEATS dates are ``P - Y`` (period-dash-year) — note the order is
    # reversed from x13read_series ("YYYYPP" → year then period).
    date_token = delim.split(data_lines[0])[0]
    pieces = date_token.split("-")
    if len(pieces) <= 1:
        msg = f"SEATS period token has unexpected format: {date_token!r}."
        raise ValueError(msg)
    p = int(pieces[0].strip())
    y = int(pieces[1].strip())
    if not isinstance(freq, YPFrequency):
        msg = f"SEATS series output requires a YP frequency, got {type(freq).__name__}."
        raise ValueError(msg)
    start = MIT.from_yp(freq, y, p)
    if len(headers) > 1:
        return MVTSeries(start, headers, vals)
    if len(headers) == 0:
        end = MIT(start.frequency, start.value + n - 1)
        return TSeries(MITRange(start, end))
    return TSeries(start, vals[:, 0])

tsecon.x13._result.x13read_workspace_table

x13read_workspace_table(
    lines: Sequence[str], *, ext: str = "nospecialrules"
) -> WorkspaceTable

Parse an X-13 multi-column tabular output into a :class:WorkspaceTable.

Mirrors Julia x13read_workspace_table at x13result.jl:398-453. The first line is a tab-separated header row; subsequent lines are tab-separated value rows. Two special-case extensions:

  • ext="acm" — the header row is missing the "lag" column; it's inserted at position 1 (0-indexed).
  • ext="rog" — the rate-of-growth table uses a different header layout that is rebuilt from line 2.
Source code in src/tsecon/x13/_result.py
def x13read_workspace_table(  # noqa: PLR0912 - mirrors Julia's per-ext branch ladder
    lines: Sequence[str], *, ext: str = "nospecialrules"
) -> WorkspaceTable:
    """Parse an X-13 multi-column tabular output into a :class:`WorkspaceTable`.

    Mirrors Julia ``x13read_workspace_table`` at ``x13result.jl:398-453``.
    The first line is a tab-separated header row; subsequent lines are
    tab-separated value rows. Two special-case extensions:

    * ``ext="acm"`` — the header row is missing the ``"lag"`` column;
      it's inserted at position 1 (0-indexed).
    * ``ext="rog"`` — the rate-of-growth table uses a different header
      layout that is rebuilt from line 2.
    """
    work_lines = list(lines)
    if work_lines and not work_lines[-1].strip():
        work_lines = work_lines[:-1]
    if not work_lines:
        return WorkspaceTable()
    headers = [_sanitize_colname(h) for h in work_lines[0].strip().split("\t")]
    if ext == "acm":
        headers.insert(1, "lag")
    elif ext == "rog":
        # Rebuild lines: replace ``\s+:`` with ``:`` and ``\s\s+`` with ``\t``.
        rog_lines = [
            re.sub(r"\s\s+", "\t", re.sub(r"\s+:", ":", line)).strip() for line in work_lines[1:]
        ]
        if not rog_lines:
            return WorkspaceTable()
        headers = ["measure"] + [_sanitize_colname(h) for h in rog_lines[0].split("\t")]
        work_lines = work_lines[:1] + rog_lines
    nrows = len(work_lines) - 2
    nrows = max(nrows, 0)
    columns: list[list[str]] = [["" for _ in range(nrows)] for _ in headers]
    for i, line in enumerate(work_lines[2:]):
        if not line.strip():
            continue
        for j, val in enumerate(line.split("\t")):
            if j >= len(headers):
                if not val.strip():
                    continue
                continue
            if i < nrows:
                columns[j][i] = val
    parsed_columns: list[Any] = []
    for col in columns:
        all_int = all(_tryparse_int(v) is not None for v in col)
        if col and all_int:
            parsed_columns.append([int(v) for v in col])
            continue
        all_float = all(_tryparse_float(v) is not None for v in col)
        if col and all_float:
            parsed_columns.append([float(v) for v in col])
            continue
        parsed_columns.append(list(col))
    ws = WorkspaceTable()
    for header, col_values in zip(headers, parsed_columns, strict=False):
        ws._c[header] = col_values
    return ws

tsecon.x13._result.x13read_key_values

x13read_key_values(
    lines: Sequence[str],
    separator: Pattern[str] | str = re.compile("[\\t:]"),
) -> Workspace

Parse an X-13 key/value output file into a :class:Workspace.

Mirrors Julia x13read_key_values at x13result.jl:316-390. The key is the text before the first separator match; the value is the stripped text after. Values are tried (in order) as: an X-13 date (only for key == "date"), an integer, a float, a vector of floats (whitespace-separated), the booleans "yes" / "no". Otherwise the raw string is kept.

The default separator is the Julia upstream's r"[\t\:]" — either a tab or a colon. The "udg" parser passes ": " instead (mirrors the upstream regex / string switch).

After all keys are loaded, :func:_add_layers rewrites dotted keys into nested workspaces.

Source code in src/tsecon/x13/_result.py
def x13read_key_values(  # noqa: PLR0912, PLR0915 - tracks Julia's per-value-type ladder
    lines: Sequence[str],
    separator: re.Pattern[str] | str = re.compile(r"[\t:]"),
) -> Workspace:
    r"""Parse an X-13 key/value output file into a :class:`Workspace`.

    Mirrors Julia ``x13read_key_values`` at ``x13result.jl:316-390``. The
    key is the text before the first separator match; the value is the
    stripped text after. Values are tried (in order) as: an X-13 date
    (only for ``key == "date"``), an integer, a float, a vector of
    floats (whitespace-separated), the booleans ``"yes"`` / ``"no"``.
    Otherwise the raw string is kept.

    The default separator is the Julia upstream's ``r"[\t\:]"`` —
    either a tab or a colon. The ``"udg"`` parser passes ``": "``
    instead (mirrors the upstream regex / string switch).

    After all keys are loaded, :func:`_add_layers` rewrites dotted keys
    into nested workspaces.
    """
    ws = Workspace()
    sep_re: re.Pattern[str] = (
        re.compile(re.escape(separator)) if isinstance(separator, str) else separator
    )
    for line in lines:
        if not line.strip():
            continue
        m = sep_re.search(line)
        if m is None and isinstance(separator, str) and separator == ": ":
            # Mirror Julia's fallback to a bare colon when ": " missed.
            m = re.compile(":").search(line)
        if m is None:
            warnings.warn(f"Could not parse: {line}", stacklevel=2)
            continue
        split_point = m.start()
        key = line[:split_point]
        val_raw = line[split_point + 1 :].strip()
        value: Any = val_raw
        found = False
        if key == "date":
            try:
                # Julia: Dates.DateFormat("u d, y") — abbreviated month name,
                # day, year. e.g. "May 20, 2026".
                value = _parse_x13_date(val_raw)
                found = True
            except ValueError:
                pass
        if not found:
            iv = _tryparse_int(val_raw)
            if iv is not None:
                value = iv
                found = True
        if not found:
            fv = _tryparse_float(val_raw)
            if fv is not None:
                value = fv
                found = True
        if not found:
            splitval = re.split(r"[\t\s]+", val_raw.replace("*******", "NaN"))
            if len(splitval) > 1:
                parsed: list[float] = []
                ok = True
                for v in splitval:
                    if not v.strip():
                        continue
                    fv2 = _tryparse_float(v.strip())
                    if fv2 is None:
                        ok = False
                        break
                    parsed.append(fv2)
                if ok and parsed:
                    value = parsed
                    found = True
        if not found and val_raw == "no":
            value = False
            found = True
        if not found and val_raw == "yes":
            value = True
            found = True
        ws._c[key] = value
    _add_layers(ws)
    return ws

tsecon.x13._result.x13read_udg

x13read_udg(file: str | PathLike[str]) -> Workspace

Parse a UDG (Census-Bureau-defined diagnostic) output file.

Mirrors Julia x13read_udg at x13result.jl:392-395. Reads the file as text, splits on newlines, and dispatches to :func:x13read_key_values with separator ": ".

Source code in src/tsecon/x13/_result.py
def x13read_udg(file: str | os.PathLike[str]) -> Workspace:
    """Parse a UDG (Census-Bureau-defined diagnostic) output file.

    Mirrors Julia ``x13read_udg`` at ``x13result.jl:392-395``. Reads
    the file as text, splits on newlines, and dispatches to
    :func:`x13read_key_values` with separator ``": "``.
    """
    text = Path(file).read_text(encoding="utf-8", errors="replace")
    lines = text.split("\n")
    return x13read_key_values(lines, separator=": ")

tsecon.x13._result.x13read_estimates

x13read_estimates(file: str | PathLike[str]) -> Workspace

Parse the X-13 .est (estimates) output file.

Mirrors Julia x13read_estimates at x13result.jl:532-560. Walks the file looking for the section markers $arima:, $regression:, $arima$estimates:, $regression$estimates:, $variance:; dispatches each section to the right sub-parser.

Source code in src/tsecon/x13/_result.py
def x13read_estimates(file: str | os.PathLike[str]) -> Workspace:
    """Parse the X-13 ``.est`` (estimates) output file.

    Mirrors Julia ``x13read_estimates`` at ``x13result.jl:532-560``.
    Walks the file looking for the section markers ``$arima:``,
    ``$regression:``, ``$arima$estimates:``, ``$regression$estimates:``,
    ``$variance:``; dispatches each section to the right sub-parser.
    """
    text = Path(file).read_text(encoding="utf-8", errors="replace")
    lines = text.split("\n")
    indices: dict[str, int] = {}
    for i, line in enumerate(lines):
        if line == "$arima:":
            indices["arima"] = i
        elif line == "$regression:":
            indices["regression"] = i
        elif line == "$arima$estimates:":
            indices["arimaestimates"] = i
        elif line == "$regression$estimates:":
            indices["regressionestimates"] = i
        elif line == "$variance:":
            indices["variance"] = i
    res = Workspace()
    # Julia ``lines[arimaestimates+1:variance-1]`` is inclusive-inclusive
    # (Julia ranges); the Python equivalent is exclusive on the upper bound,
    # so ``[start:variance]`` (not ``variance-1``) keeps the same elements.
    if "arima" in indices and "arimaestimates" in indices and "variance" in indices:
        res._c["arima"] = x13read_workspace_table(
            lines[indices["arimaestimates"] + 1 : indices["variance"]]
        )
        res._c["variance"] = x13read_key_values(lines[indices["variance"] + 1 :])
    elif "regression" in indices and "regressionestimates" in indices and "variance" in indices:
        res._c["regression"] = x13read_workspace_table(
            lines[indices["regressionestimates"] + 1 : indices["variance"]]
        )
        res._c["variance"] = x13read_key_values(lines[indices["variance"] + 1 :])
    return res

tsecon.x13._result.x13read_model

x13read_model(file: str | PathLike[str]) -> Workspace

Parse the X-13 .mdl (model) output file.

Mirrors Julia x13read_model at x13result.jl:579-613. The file contains zero, one, or both of an arima{model=...} block and a regression{...} block; each is dispatched to :func:_x13read_model_block.

Source code in src/tsecon/x13/_result.py
def x13read_model(file: str | os.PathLike[str]) -> Workspace:
    """Parse the X-13 ``.mdl`` (model) output file.

    Mirrors Julia ``x13read_model`` at ``x13result.jl:579-613``. The
    file contains zero, one, or both of an ``arima{model=...}`` block
    and a ``regression{...}`` block; each is dispatched to
    :func:`_x13read_model_block`.
    """
    text = Path(file).read_text(encoding="utf-8", errors="replace")
    lines = text.split("\n")
    indices: dict[str, int] = {}
    for i, line in enumerate(lines):
        s = line.strip()
        if s in {"arima{model=", "arima{"}:
            indices["arima"] = i
        elif s == "regression{":
            indices["regression"] = i
    res = Workspace()
    if "arima" in indices:
        end = len(lines)
        if "regression" in indices:
            end = (
                len(lines)
                if indices["arima"] > indices["regression"]
                else indices["regression"] - 1
            )
        res._c["arima"] = _x13read_model_block(lines[indices["arima"] : end])
    if "regression" in indices:
        end = len(lines)
        if "arima" in indices:
            end = len(lines) if indices["regression"] > indices["arima"] else indices["arima"] - 1
        res._c["regression"] = _x13read_model_block(lines[indices["regression"] : end])
    return res

tsecon.x13._result.x13read_identify

x13read_identify(file: str | PathLike[str]) -> Workspace

Parse the X-13 .iac / .ipc (identify) output files.

Mirrors Julia x13read_identify at x13result.jl:562-577. The file contains 1+ pages, each beginning with a $diff = N / $sdiff = M pair followed by an autocorrelation / partial-autocorrelation table.

Source code in src/tsecon/x13/_result.py
def x13read_identify(file: str | os.PathLike[str]) -> Workspace:
    """Parse the X-13 ``.iac`` / ``.ipc`` (identify) output files.

    Mirrors Julia ``x13read_identify`` at ``x13result.jl:562-577``. The
    file contains 1+ pages, each beginning with a ``$diff = N`` /
    ``$sdiff = M`` pair followed by an autocorrelation /
    partial-autocorrelation table.
    """
    text = Path(file).read_text(encoding="utf-8", errors="replace")
    lines = text.split("\n")
    page_locs: list[int] = []
    for i, line in enumerate(lines):
        if len(line) > 4 and line[:5] == "$diff":
            page_locs.append(i)
    res = Workspace()
    for i, loc in enumerate(page_locs):
        diff_label = lines[loc].replace("$", "").replace("= ", "").strip()
        sdiff_label = lines[loc + 1].replace("$", "").replace("= ", "").strip()
        sym = f"{diff_label}{sdiff_label}"
        # Same inclusive-vs-exclusive Julia/Python translation as
        # ``x13read_estimates`` above: ``page_locs[i+1] - 1`` in Julia
        # (last line of the current page) becomes ``page_locs[i+1]`` in
        # Python (exclusive on the next page's start).
        end_idx = page_locs[i + 1] if i + 1 < len(page_locs) else len(lines)
        res._c[sym] = x13read_workspace_table(lines[loc + 2 : end_idx])
    return res

tsecon.x13._result.x13read_err

x13read_err(
    file: str | PathLike[str],
    warnings_out: list[str],
    notes_out: list[str],
    errors_out: list[str],
) -> None

Parse the X-13 .err output file into warnings / notes / errors lists.

Mirrors Julia x13read_err at x13result.jl:287-313. Each diagnostic begins with " WARNING:" / " ERROR:" / " NOTE:" (note the leading space); continuation lines are appended to the last diagnostic with \n separators. The three output lists are mutated in place to mirror the Julia push! shape.

Source code in src/tsecon/x13/_result.py
def x13read_err(
    file: str | os.PathLike[str],
    warnings_out: list[str],
    notes_out: list[str],
    errors_out: list[str],
) -> None:
    r"""Parse the X-13 ``.err`` output file into warnings / notes / errors lists.

    Mirrors Julia ``x13read_err`` at ``x13result.jl:287-313``. Each
    diagnostic begins with ``" WARNING:"`` / ``" ERROR:"`` / ``" NOTE:"``
    (note the leading space); continuation lines are appended to the
    last diagnostic with ``\n`` separators. The three output lists are
    mutated in place to mirror the Julia ``push!`` shape.
    """
    text = Path(file).read_text(encoding="utf-8", errors="replace")
    lines = text.split("\n")
    collected: list[str] = []
    for line in lines:
        if len(line) >= 11 and (
            line.startswith(" WARNING:") or line.startswith(" ERROR:") or line.startswith(" NOTE:")
        ):
            collected.append(line)
        elif collected and line:
            # Continuation lines extend the last diagnostic (Julia
            # ``x13result.jl:296-301``); the Julia version also appends
            # bare trailing-newline empty lines, but those carry no
            # information so the Python port skips them — matches the
            # Julia *intent* without the trailing-whitespace artefact.
            collected[-1] = collected[-1] + "\n" + line
    for line in collected:
        if line.startswith(" WARNING:"):
            warnings_out.append(line[10:])
        elif line.startswith(" ERROR:"):
            errors_out.append(line[8:])
        elif line.startswith(" NOTE:"):
            notes_out.append(line[7:])

License notice

The X-13ARIMA-SEATS binary vendored in the wheels is a U.S. Government work and not subject to U.S. copyright (17 U.S.C. §105). The full Census Bureau disclaimer ships in the wheel at tsecon/x13/X13AS_LICENSE.txt and is reproduced in the project LICENSE file's third-party section. The Python wrapper code is MIT-licensed.