Code map: every module of src/tramdag/¶
This page lists each module with its role and the decisions behind it. The API pages document every signature and default. Names in parentheses are private machinery and are not part of the API.
spec.py: declare the model¶
Every term is a Term subclass under its pythonic name. The paper's symbol is
the same object, so LS is LinearShift. A spec is plain data: it holds no
tensors, so it serializes and compares without torch.
| Name | Role |
|---|---|
Term |
One additive term of a node's transformation. + on terms builds plain lists. The contract for a custom term is in architecture.md. |
SI() / CI() / Intercept |
Intercept (I) is the one intercept class. SI and CI are its two spellings with the arity checked. |
LinearShift / ComplexShift / VaryingCoefficient |
The three shifts: an interpretable coefficient, a network, and the penalized treatment-effect head (varying-coefficients.md). |
ContinuousNode / OrdinalNode |
The two node kinds. |
validate_and_sort() |
Edge-ownership validation plus Kahn topological sort. The order makes the flow triangular. Kahn's sort emits ready nodes in sorted batches, so the order is deterministic. |
spec_to_dict() / spec_from_dict() |
Checkpoint (de)serialization. A custom term writes its import path, so a checkpoint loads without a registry. strict=True refuses a term that omits a shaping option (Term.acting_options). |
transforms.py: the monotone map h and the ordinal transform¶
| Name | Role |
|---|---|
StandardLogistic |
The TRAM base distribution. zuko has none (zuko-upstream.md). |
BernsteinUT / SplineUT / AffineUT |
The three transforms of a continuous node (model.md). |
make_univariate_transform() |
Name to transform instance. |
ordinal_log_prob() and the other ordinal_* functions |
The ordinal likelihood and its inverses. The log probability is in log space (model.md). |
flow.py: the model¶
CausalFlowDAG holds one
Node per variable in topological order. Construction
seeds each node's weights from the constructor seed and the node's name. A node
starts from the same weights in every spec that contains it. Node(seed=) uses
the same stream, so a lone node starts from the weights it has in the flow.
Without a seed, the weights come from torch's global RNG. nodes.seeded holds
this seeding. It runs in torch.random.fork_rng, so construction does not
change torch's global RNG state. Each random act takes its own seed: the
constructor for the weights, fit(seed=) for the shuffle of every node,
sample(seed=) and abduct(seed=) for the latent draws. The class composes
FitMixin and ReadoutsMixin. The modules below list their methods.
CausalFlowDAG.outcome()gives the one node without children.samplecovers observational, interventional (do=, graph mutilation) and counterfactual (u=) sampling.abductrecovers the latents.pmfanddensitygive the analytic conditionals.saveandloadwrite checkpoints with the nodes' histories and provenance.loadrequires a complete checkpoint and fails loudly otherwise.- The flow calibrates every node before the first fit and rolls the flags
back when a node refuses the frame. A term names, validates and recomputes
its own side columns through the
ShiftModulehooks, and the flow fits the binary propensity.
modules.py: the term modules¶
There is one module class per term, and the term class holds it as module
(ComplexShift.module is ComplexShiftModule). The module holds the term's
network and owns the runtime hooks (architecture.md).
The default architectures replicate the PyTorch reference
buehlpa/TramDag, file tram_models.py,
so a fitted model stays comparable to it. They are not the paper's nets, which
paper-replication.md lists per experiment.
| Name | Role |
|---|---|
ShiftModule / InterceptModule |
The behavior hooks a term module owns. |
LinearShiftModule |
A linear layer without bias, because the intercept slot owns the constant. .weight is the interpretable coefficient. |
ComplexShiftModule / VaryingCoefficientModule |
The network shift and the varying-coefficient head. |
intercept_module() |
The one factory for the intercept slot: a free theta, one joint net, or one net per parent summed in coefficient space. |
nodes.py: the node model¶
Node is one sub-model per variable, a TRAM regression
on its parents. tramdag.Node exports it. It builds its terms through
each term's module. The continuous-versus-ordinal branches live in its
methods log_prob, sample, abduct, marginal_theta and encode
(architecture.md). The shared helpers (encode,
schema_entry, tensorize, the column checks and the checkpoint IO) serve
both the node and the flow.
fitting.py: NodeFitMixin and FitMixin¶
Node.fit() and
Node.fit_classical() fit one node,
through NodeFitMixin. The flow's fit() and
fit_classical() loop over the
nodes, through FitMixin, serially or in n_jobs forked processes.
fit_jointly() trains all nodes in
one loop; it shares _train_loop with Node.fit. The
mechanics are in fitting.md.
readouts.py: ReadoutsMixin¶
The read-out methods (varying_coef, ls_coefficients, to_matrix,
intercept_contributions, design_matrix, shift_curve) live here
once and are methods of the flow through the mixin.
scores.py: effect-modifier detection¶
node_scores() and
effect_modifier_scan() are the
analytic scores and the fluctuation scan (scores.md). The
per-term score columns come from each term's score_columns hook.
callbacks.py: the shipped fit callbacks¶
EarlyStopping is the only shipped
callback. Anything else is torch's lr_scheduler and a few lines of callback
(fitting.md).
plots.py: the figures¶
matplotlib is optional (tramdag[plots]) and the first plot
call imports it, never the package import.
plot_dag(),
plot_marginals(),
plot_training() and
plot_varying_coef() each take ax= to
draw into a given layout. plot_dag draws every edge by the term that owns
it.
What is not in the package¶
Three groups of files are research code:
- the SCM generators
- the frozen datasets
- the replication scripts
They live in experiments/, outside the installed package;
experiments/README.md describes them and
tests/README.md what the framework tests measure
instead.
Decisions behind the training defaults¶
The signatures hold the defaults. This list keeps the reasons.
- Weight initialization is
CausalFlowDAG(init=). Initialization happens at construction, so the constructor seed makes the initial weights reproducible."torch"is thenn.Lineardefault."glorot"is the KerasDensedefault, the paper's reference, and it is decisive under the paper's full-batch protocol.zero_shift_networks=Truestarts everyCSshift at 0. - Network widths and
activation=follow the default classes of the PyTorch reference, for parity. TheVChead has no counterpart there."sigmoid"and"tanh"are the paper's activations. batch_norm=is off, because neither reference uses it. When on, it needs more than one row per batch, and inference needseval()mode.fitandloadleave the flow there.- The transform domain is
transforms.BOUND, a constant. The order of the Bernstein coefficients is in zuko-upstream.md.