Skip to content

Add native dead and wrong-polarity BPM error model#44

Open
thellert wants to merge 1 commit into
kparasch:mainfrom
als-apg:pr/bpm-dead-polarity-error-model
Open

Add native dead and wrong-polarity BPM error model#44
thellert wants to merge 1 commit into
kparasch:mainfrom
als-apg:pr/bpm-dead-polarity-error-model

Conversation

@thellert

Copy link
Copy Markdown

Motivation

Realistic commissioning simulations need to model imperfect BPMs:

  • Dead BPMs — channels that report only amplified noise instead of a real
    position.
  • Reversed polarity — channels whose measured sign is inverted on one plane.

Today these can only be emulated by post-processing BPM output outside the
library. This PR makes both first-class, YAML-configurable BPM errors, applied
consistently through every reading path.


What the feature does

BPMSystem (pySC/core/bpm_system.py)

  • Three new optional per-BPM arrays: dead (bool), polarity_x, polarity_y
    (float, default all-ones). They default to None and are populated during
    configuration; existing construction is unaffected.
  • polarity_x/polarity_y are added to the ones-initialised BPM field set, and
    initialize_empty_arrays() fills them consistently with the other per-BPM
    arrays.
  • In all three capture paths (capture_orbit, capture_injection,
    capture_kick):
    • the measured signal is multiplied by the per-plane polarity before noise
      is added (a None polarity is treated as unity, so unconfigured systems are
      unchanged);
    • dead BPMs are overwritten with amplified noise after gain corrections,
      guarded by an alive mask (see below).

Dead-BPM alive-mask guard

A dead BPM should only fabricate a noise reading where the beam actually reached
it. Downstream of a beam-loss point the reading is already NaN (the beam never
arrived), and overwriting it with noise*10 invents phantom readings that
inflate transmission/reach metrics. The override is therefore masked:

if self.dead is not None and self.dead.any():
    dead_alive_x = self.dead & ~np.isnan(fake_x)
    dead_alive_y = self.dead & ~np.isnan(fake_y)
    fake_x[dead_alive_x] = noise_x[dead_alive_x] * 10
    fake_y[dead_alive_y] = noise_y[dead_alive_y] * 10

Dead BPMs upstream of the loss still emit amplified noise; dead BPMs downstream
stay NaN.

Configuration (pySC/configuration/bpm_system_conf.py)

configure_bpms reads two optional per-BPM-category fractions:

  • fraction_dead → marks max(1, round(n·fraction)) BPMs in the category dead
    and registers them in SC.tuning.bad_bpms;
  • fraction_wrong_polarity → flips max(1, round(n·fraction)) BPMs to -1.0
    polarity, drawn independently for x and y.

RNG (pySC/core/rng.py)

Adds RNG.choice(a, size=None, replace=True), a thin seeded wrapper over the
underlying generator's choice, used for the reproducible random subset
selection above. This keeps all randomness routed through the seeded RNG so
runs remain reproducible.


Scope / compatibility

  • Additive only: unconfigured BPM systems behave exactly as before (dead unset,
    polarities unity). No existing public behavior changes.
  • initialize_empty_arrays() is preserved and extended, not removed.
  • No change to the shared array type or serializer; the new fields rely on the
    existing optional-field handling.

Validation

  • New/adapted regression tests cover: the downstream-loss NaN guard on the
    tracking path (capture_injection) and as a capture_orbit guard unit test;
    and the configuration invariants (dead count, bad_bpms membership, -1.0
    polarity), written to be robust to RNG draw order.
  • The full test suite passes with no new failures relative to main.
  • The diff touches only pySC/core/bpm_system.py,
    pySC/configuration/bpm_system_conf.py, pySC/core/rng.py, and the BPM test
    modules.

Model dead BPMs and per-plane polarity reversal as config-driven BPM errors.
BPMSystem gains optional dead, polarity_x and polarity_y arrays; each capture
path multiplies the signal by the polarity before noise and overwrites dead
BPMs with amplified noise, guarded by an alive mask so a dead BPM downstream of
beam loss stays NaN instead of fabricating a reading. configure_bpms applies
fraction_dead and fraction_wrong_polarity per BPM category and registers dead
BPMs in bad_bpms. Add RNG.choice for reproducible subset selection. Include
regression tests for the guard and the configuration invariants.

@kparasch kparasch left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a very nice model to include. I have only requested some minor changes I would like

n_dead = max(1, round(len(cat_indices) * fraction_dead))
dead_idx = SC.rng.choice(len(cat_indices), size=n_dead, replace=False)
SC.bpm_system.dead[cat_indices[dead_idx]] = True
SC.tuning.bad_bpms.extend(cat_indices[dead_idx].tolist())

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dead bpms should not be passed to SC.tuning.bad_bpms . The user should declare the bad bpms themselves through appropriate measurements

Comment thread pySC/core/bpm_system.py

dead: Optional[NPARRAY] = None
polarity_x: Optional[NPARRAY] = None
polarity_y: Optional[NPARRAY] = None

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These can be:

dead:  NPARRAY] = np.array([])
polarity_x:  NPARRAY] = np.array([])
polarity_y: NPARRAY] = np.array([])

Comment thread pySC/core/bpm_system.py
for field in BPM_FIELDS_TO_INITIALISE_ONES:
if not len(getattr(self, field)): # array is empty
value = getattr(self, field)
if value is None or not len(value): # optional/unset or empty array

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for this if dead, polarity_x, polarity_y is declared as np.array([]) by default

Comment thread pySC/core/bpm_system.py
fake_orbit_x = (rotated_orbit[0] - self.offsets_x) * (1 + self.calibration_errors_x) + noise_x
fake_orbit_y = (rotated_orbit[1] - self.offsets_y) * (1 + self.calibration_errors_y) + noise_y
pol_x = self.polarity_x if self.polarity_x is not None else 1.0
pol_y = self.polarity_y if self.polarity_y is not None else 1.0

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for the if-clause, polarity_x, polarity_y will always be initialized.

Comment thread pySC/core/bpm_system.py

fake_trajectory_x_tbt = np.zeros([len(self.indices), n_turns])
fake_trajectory_y_tbt = np.zeros([len(self.indices), n_turns])
pol_x = self.polarity_x if self.polarity_x is not None else 1.0

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same: no need for the if-clause, polarity_x, polarity_y will always be initialized.

Comment thread pySC/core/bpm_system.py

fake_trajectory_x_tbt = np.zeros([len(self.indices), n_turns])
fake_trajectory_y_tbt = np.zeros([len(self.indices), n_turns])
pol_x = self.polarity_x if self.polarity_x is not None else 1.0

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same: no need for the if-clause, polarity_x, polarity_y will always be initialized.

Comment thread pySC/core/bpm_system.py
if self.dead is not None and self.dead.any():
dead_alive_x = self.dead & ~np.isnan(fake_x)
dead_alive_y = self.dead & ~np.isnan(fake_y)
fake_x[dead_alive_x] = noise_x[dead_alive_x] * 10

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the hard-coded "10" factor here. Maybe it can be declared as one of the fields in the BPMSystem class:

class BPMSystem(BaseModel, extra='forbid'):
...
dead_bpm_noise_factor: float = 10
...

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants