Skip to content

Builder APIs disagree on setter naming, entry, and terminal method #434

Open

Description

@ciaranra

Problem

PECOS has fifteen Python-facing builders and they disagree on all three parts of the
builder contract: how setters are named, how the builder is entered, and how it is
terminated. The split is close to even, so a reader cannot infer any of it.

Setter naming: 8 versus 7

with_* bare
general_noise() (47 methods) sim(...)
depolarizing_noise() (7) DetectorErrorModel.builder() (14)
biased_depolarizing_noise() (7) BpOsdBuilder (5)
DemBuilder (6) BpLsdBuilder (4)
DemSamplerBuilder (4) RelayBpBuilder (8)
InfluenceBuilder (4) MinSumBpBuilder (3)
SurfacePatchBuilder (3) UnionFindBuilder (1)
ColorCode488Builder (1)

Two plausible dividing lines both fail:

  • user-facing versus low-level — the noise builders are user-facing and use with_*,
    while sim() is user-facing and bare.
  • Rust-backed versus Python — BpOsdBuilder is Rust-backed and bare, DemBuilder is
    Rust-backed and with_*.

Terminal method

  • .build() — most builders
  • .run() — sim(...)
  • none — the noise builders, which are passed directly into .noise(...)

Entry

  • free function: sim(...), general_noise(), depolarizing_noise()
  • classmethod: DetectorErrorModel.builder()
  • direct construction with required positional arguments: BpOsdBuilder(pcm, error_rate)

Why now

PR #420 added DetectorErrorModel.builder() and chose bare setters partly on the
strength of a "user-facing builders use bare setters" reading of the codebase. That
rule does not hold
— see the counterexamples above. The choice is defensible on other
grounds (it is scalar configuration on a Python surface, and the Rust API Guidelines
leave builder naming explicitly open, reserving with_ for constructors such as
Vec::with_capacity), but it was made against a pattern that turned out to be imaginary.
Rather than let that stand as an accidental precedent, the project should decide.

Proposal

  1. Pick one setter convention and write it down where it will be found — a short
    section in the contributing or API-conventions docs, not just an issue comment.
  2. Standardize the terminal. .build() everywhere, with sim(...).run() as a
    deliberate, documented exception because it also executes.
  3. Decide whether noise builders should have a terminal at all, or remain
    builder-shaped objects consumed directly by .noise(...).
  4. Converge entries on one shape, or state the rule that selects between them.
  5. Migrate with aliases and a deprecation ramp, as in Deprecation ramp: align pecos-engines GeneralNoiseModel idle-noise API with the structured vocabulary #424 — several of these are public
    and widely used.

Notes

Activity

  1. added
    enhancementNew feature or request
    rustPull requests that update rust code
    pythonPull requests that update python code
    on Aug 4, 2026
  2. ciaranra commented on Aug 4, 2026

    @ciaranra
    MemberAuthor

    A dividing line that fits the data, arising from the #433 decision (NoiseParameters adopting with_* to match the sim() noise builders):

    • Parameter/configuration objects use with_* and have no terminal. They describe a thing and are handed to something else: general_noise(), depolarizing_noise(), biased_depolarizing_noise(), and NoiseParameters once renamed. All are passed into .noise(...).
    • Orchestration builders that run a pipeline use bare setters and a terminal. They do something at the end: sim(...).run(), DetectorErrorModel.builder().build().

    This is a real distinction rather than a post-hoc rationalization — the two groups differ in what they return and how they are consumed, not just in age or implementation language.

    It does not cover everything, and the leftovers should be decided explicitly rather than quietly:

    • DemBuilder, DemSamplerBuilder, InfluenceBuilder are pipeline builders with terminals but use with_*. Under this rule they would migrate to bare — but they are the low-level/advanced surfaces (DemBuilder's own docstring points users to from_circuit()), so grandfathering them is defensible if stated.
    • SurfacePatchBuilder and ColorCode488Builder construct code objects; they fit "orchestration" better than "configuration".
    • The decoder builders (BpOsdBuilder and siblings) are already bare with .build(), so they satisfy the rule as-is.

    If this line is adopted, the DetectorErrorModel.builder() added in #428 is already correct, and NoiseParameters moves to with_* under #433.

  3. ciaranra commented on Aug 4, 2026

    @ciaranra
    MemberAuthor

    The parameter-object half of this is settled by a hard constraint, not taste.

    NoiseParameters is a dataclass whose fields are public attributes (noise.p1, noise.p_meas, noise.p_idle_linear). A bare setter named p1() would shadow the field of the same name, so bare setters are not available for a parameter object at all — with_<field> is forced.

    That also explains why general_noise() had freedom the parameter object does not: it is a builder with no public fields of those names.

    So the target convention is:

    • Parameter objects (public fields): with_<field_name>, no terminal. Forced by the attribute collision. NoiseParameters now follows this.
    • Orchestration builders (no public fields): bare setters plus a terminal. sim(...).run(), DetectorErrorModel.builder().build().

    general_noise(), depolarizing_noise(), and biased_depolarizing_noise() already use the with_ prefix, so they are aligned on that axis. The remaining divergence is the suffix: they spell it with_p1_probability / with_p_idle_linear_rate where NoiseParameters uses with_p1 / with_p_idle_linear.

    Convergence work, for #425:

    1. Add suffix-free aliases (with_p1, with_p2, with_meas/with_p_meas, with_prep/with_p_prep) to the simulator noise builders, matching the field-name rule.
    2. Deprecate the suffixed spellings on a ramp, as in Deprecation ramp: align pecos-engines GeneralNoiseModel idle-noise API with the structured vocabulary #424.
    3. Settle the one genuine semantic mismatch while doing it: the simulator's with_p_idle_linear_rate corresponds to what is our deprecated Z-only alias p_idle_linear_rate, not to the family scalar p_idle_linear. Renaming without resolving that would create a false equivalence between two different quantities.

    Item 3 is the reason this cannot be a mechanical find-and-replace.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestpythonPull requests that update python coderustPull requests that update rust code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions