Skip to content

Commit 90f7760

Browse files
authored
Error calculation action and bbq (#381)
* Tune error based on deviation of filtered BBQ data to the moving average * Action error calculated from error on the spectral line
1 parent 61da514 commit 90f7760

File tree

16 files changed

+323
-120
lines changed

16 files changed

+323
-120
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# OMC3 Changelog
22

3+
#### 2022-11-08 - v0.7.0 - _jdilly_
4+
5+
- Added:
6+
- Tune error based on deviation of filtered BBQ data to the moving average
7+
(over moving average window)
8+
- Action error calculated from error on the spectral line
9+
(which in turn is the same as NOISE)
10+
311
#### 2022-11-01 - v0.6.6
412

513
- Bugfixes

omc3/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
__title__ = "omc3"
1212
__description__ = "An accelerator physics tools package for the OMC team at CERN."
1313
__url__ = "https://github.com/pylhc/omc3"
14-
__version__ = "0.6.6"
14+
__version__ = "0.7.0"
1515
__author__ = "pylhc"
1616
__author_email__ = "[email protected]"
1717
__license__ = "MIT"

omc3/amplitude_detuning_analysis.py

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -115,29 +115,27 @@
115115
116116
117117
"""
118-
import os
119-
from collections import OrderedDict
120118
from datetime import timedelta
121119
from pathlib import Path
122120
from typing import List, Sequence, Tuple, Dict, Any, Union
123121

124122
import numpy as np
125123
import tfs
126124
from generic_parser import DotDict
127-
from generic_parser.entrypoint_parser import EntryPointParameters, entrypoint, save_options_to_config
125+
from generic_parser.entrypoint_parser import EntryPointParameters, entrypoint
128126
from numpy.typing import ArrayLike
129127
from tfs.frame import TfsDataFrame
130128

131-
from omc3.definitions import formats
132129
from omc3.definitions.constants import PLANES
133130
from omc3.tune_analysis import fitting_tools, kick_file_modifiers, timber_extract
134-
from omc3.tune_analysis.bbq_tools import OutlierFilterOpt, MinMaxFilterOpt
131+
from omc3.tune_analysis.bbq_tools import OutlierFilterOpt, MinMaxFilterOpt, FilterOpts
135132
from omc3.tune_analysis.constants import (
136133
get_bbq_col,
137134
get_bbq_out_name,
138135
get_kick_out_name,
139-
get_mav_col,
140-
get_timber_bbq_key, INPUT_KICK, INPUT_PREVIOUS, CORRECTED,
136+
get_timber_bbq_key,
137+
get_natq_err_col,
138+
INPUT_KICK, INPUT_PREVIOUS, CORRECTED,
141139
)
142140
from omc3.tune_analysis.kick_file_modifiers import (
143141
read_timed_dataframe,
@@ -266,7 +264,8 @@ def analyse_with_bbq_corrections(opt: DotDict) -> Tuple[TfsDataFrame, TfsDataFra
266264

267265
opt, filter_opt = _check_analyse_opt(opt)
268266
kick_df, bbq_df = get_kick_and_bbq_df(kick=opt.kick, bbq_in=opt.bbq_in,
269-
beam=opt.beam, filter_opt=filter_opt, output=opt.output)
267+
beam=opt.beam,
268+
filter_opt=filter_opt)
270269

271270
kick_plane = opt.plane
272271

@@ -277,23 +276,34 @@ def analyse_with_bbq_corrections(opt: DotDict) -> Tuple[TfsDataFrame, TfsDataFra
277276
kick_df = double_action_analysis(kick_df, opt.detuning_order, corrected)
278277

279278
if opt.output:
280-
LOG.info(f"Writing kick data to file in directory '{opt.output.absolute()}'")
281-
opt.output.mkdir(parents=True, exist_ok=True)
282-
write_timed_dataframe(opt.output / get_kick_out_name(), kick_df)
283-
279+
_write_dataframes(opt.output, kick_df, bbq_df)
284280
return kick_df, bbq_df
285281

286282

287283
def get_kick_and_bbq_df(kick: Union[Path, str], bbq_in: Union[Path, str],
288-
beam: int = None, filter_opt = None,
289-
output: Path = None
284+
beam: int = None,
285+
filter_opt: FilterOpts = None,
290286
) -> Tuple[tfs.TfsDataFrame, tfs.TfsDataFrame]:
291287
"""Load the input data."""
292288
bbq_df = None
293289
if bbq_in is not None and bbq_in == INPUT_PREVIOUS:
290+
# NOTE: this is not the same as the "previous BBQ data" option in the GUI.
291+
# That one just uses the previous bbq_ampdet.tfs file (loaded in the "else" below).
292+
# The use-case for the INPUT_PREVIOUS option here is,
293+
# that you can modify the kick_ampdet_xy file manually (e.g. removing kicks)
294+
# and run the fitting on the new data again,
295+
# without having to touch the whole BBQ stuff again (as the values are already in the file).
296+
# Tips:
297+
# - Remove full columns to get rid of the whole kick
298+
# - Add NaNs into NATQ columns you want to ignore (in case you want to keep the other plane for this kick)
299+
# - Add NaNs to the ERRNATQ columns if you want to plot the point (w/o error bars) but not use it for fit
294300
LOG.debug("Getting data from previous ampdet kick file")
295301
kick_df = read_timed_dataframe(Path(kick) / get_kick_out_name())
296302
kick_df.headers = {k: v for k, v in kick_df.headers.items() if not k.startswith("ODR_")}
303+
304+
# redo the corrected columns, so you only need to add NaNs into the NATQ columns
305+
LOG.debug("Adding corrected natural tunes and stdev to kick data")
306+
kick_df = kick_file_modifiers.add_corrected_natural_tunes(kick_df)
297307
else:
298308
LOG.debug("Getting data from kick files")
299309
kick_df = read_two_kick_files_from_folder(kick)
@@ -307,15 +317,6 @@ def get_kick_and_bbq_df(kick: Union[Path, str], bbq_in: Union[Path, str],
307317
LOG.debug("Adding corrected natural tunes and stdev to kick data")
308318
kick_df = kick_file_modifiers.add_corrected_natural_tunes(kick_df)
309319

310-
if output:
311-
LOG.info(f"Writing BBQ data to file in directory '{output.absolute()}'")
312-
try:
313-
window = filter_opt.window
314-
except AttributeError:
315-
window = filter_opt[0].window
316-
317-
x_interval = get_approx_bbq_interval(bbq_df, kick_df.index, window)
318-
write_timed_dataframe(output / get_bbq_out_name(), bbq_df.loc[x_interval[0]: x_interval[1]])
319320
return kick_df, bbq_df
320321

321322

@@ -419,7 +420,7 @@ def get_approx_bbq_interval(
419420
# Private Functions ------------------------------------------------------------
420421

421422

422-
def _check_analyse_opt(opt: DotDict):
423+
def _check_analyse_opt(opt: DotDict) -> Tuple[DotDict, FilterOpts]:
423424
"""Perform manual checks on opt-sturcture."""
424425
LOG.debug("Checking Options.")
425426

@@ -542,6 +543,15 @@ def _get_ampdet_data_as_array(data: Dict[Any, AmpDetData], column: str) -> Array
542543
return np.vstack([getattr(d, column) for d in data.values()])
543544

544545

546+
def _write_dataframes(output: Path, kick_df: TfsDataFrame, bbq_df: TfsDataFrame):
547+
LOG.info(f"Writing kick data to file in directory '{output.absolute()}'")
548+
output.mkdir(parents=True, exist_ok=True)
549+
write_timed_dataframe(output / get_kick_out_name(), kick_df)
550+
if bbq_df is not None:
551+
LOG.info(f"Writing BBQ data to file in directory '{output.absolute()}'")
552+
write_timed_dataframe(output / get_bbq_out_name(), bbq_df)
553+
554+
545555
# Script Mode ##################################################################
546556

547557

omc3/harpy/handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ def _compute_headers(panda, date=None):
159159
pass
160160
else:
161161
headers[f"{prefix}Q{PLANE_TO_NUM[plane]}"] = np.mean(bpm_tunes)
162-
headers[f"{prefix}Q{PLANE_TO_NUM[plane]}RMS"] = np.std(bpm_tunes)
162+
headers[f"{prefix}Q{PLANE_TO_NUM[plane]}RMS"] = np.std(bpm_tunes) # TODO: not really the RMS?
163163
if date:
164164
headers["TIME"] = date.strftime(formats.TIME)
165165
return headers

omc3/optics_measurements/constants.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
ORBIT_NAME: str = "orbit_"
1818
KICK_NAME: str = "kick_"
1919
IP_NAME: str = "interaction_point_"
20+
CALIBRATION_FILE: str = "calibration_{plane}.out"
2021

2122
# Column Names -----------------------------------------------------------------
2223
# Pre- and Suffixe
@@ -40,20 +41,28 @@
4041
DPP: str = "DPP"
4142
DPPAMP: str = "DPPAMP"
4243
AMPLITUDE: str = "AMP"
44+
NAT_AMPLITUDE: str = "NATAMP"
4345
PHASE: str = "PHASE"
4446
PHASE_ADV: str = "MU"
4547
F1001: str = "F1001"
4648
F1010: str = "F1010"
49+
NOISE: str = "NOISE"
50+
CLOSED_ORBIT: str = "CO"
4751

4852
SECONDARY_AMPLITUDE_X: str = "AMP01_X" # amplitude of secondary line in horizontal spectrum
4953
SECONDARY_AMPLITUDE_Y: str = "AMP10_Y" # amplitude of secondary line in vertical spectrum
5054
SECONDARY_FREQUENCY_X: str = "PHASE01_X" # frequency of secondary line in horizontal spectrum
5155
SECONDARY_FREQUENCY_Y: str = "PHASE10_Y" # frequency of secondary line in vertical spectrum
5256

57+
# Kick files
5358
TIME: str = "TIME"
5459
ACTION: str = "2J"
5560
SQRT_ACTION: str = "sqrt2J"
5661

62+
# Calibration files
63+
CALIBRATION = "CALIBRATION"
64+
ERR_CALIBRATION = "ERROR_CALIBRATION"
5765

5866
# Headers ----------------------------------------------------------------------
5967
RESCALE_FACTOR: str = "RescalingFactor"
68+
BPM_RESOLUTION: str = "BPMResolution"

omc3/optics_measurements/kick.py

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
NAT_TUNE, PEAK2PEAK,
2121
RES,
2222
RESCALE_FACTOR, RMS,
23-
SQRT_ACTION, TIME, TUNE, S)
23+
SQRT_ACTION, TIME, TUNE, S, NOISE, CLOSED_ORBIT)
24+
from omc3.utils.stats import weighted_mean, weighted_error
2425

2526

2627
def calculate(measure_input, input_files, scale, header_dict, plane):
@@ -37,7 +38,7 @@ def calculate(measure_input, input_files, scale, header_dict, plane):
3738
`TfsDataFrame` containing actions and their errors.
3839
"""
3940
try:
40-
kick_frame = _getkick(measure_input, input_files, plane)
41+
kick_frame = _get_kick(measure_input, input_files, plane)
4142
except IndexError: # occurs if either no x or no y files exist
4243
return pd.DataFrame
4344
kick_frame = _rescale_actions(kick_frame, scale, plane)
@@ -58,7 +59,7 @@ def _rescale_actions(df, scaling_factor, plane):
5859
return df
5960

6061

61-
def _getkick(measure_input, files, plane):
62+
def _get_kick(measure_input, files, plane):
6263
load_columns, calc_columns, column_types = _get_column_mapping(plane)
6364
kick_frame = pd.DataFrame(data=0.,
6465
index=range(len(files[plane])),
@@ -71,22 +72,54 @@ def _getkick(measure_input, files, plane):
7172
kick_frame.loc[i, col] = df[src]
7273

7374
# calculate data from measurement
74-
kick_frame.loc[i, calc_columns] = _gen_kick_calc(measure_input, df, plane)
75+
kick_frame.loc[i, calc_columns] = _get_action(measure_input, df, plane)
7576
kick_frame = kick_frame.astype(column_types)
7677
return kick_frame
7778

7879

79-
def _gen_kick_calc(meas_input, lin, plane):
80+
def _get_action(meas_input, lin: pd.DataFrame, plane: str) -> np.ndarray:
8081
"""
81-
Takes either PK2PK/2 for kicker excitation or AMP for AC-dipole excitation
82+
Calculates action (2J and sqrt(2J)) and its errors from BPM data in lin-df.
83+
Takes either PK2PK/2 for kicker excitation or AMP for AC-dipole excitation,
84+
as the amplitude of the oscillations for single kicks falls off over turns,
85+
and hence the amplitude of the main line does not represent the initial kick,
86+
whereas it is constant for the driven excitation.
87+
Reminder: A = sqrt(2J \beta) .
88+
89+
TODO (jdilly 07.09.2022):
90+
beta_phase instead of beta_model as stated below Eq. (11) in
91+
PHYS. REV. ACCEL. BEAMS 23, 042801 (2020)
92+
93+
Returns:
94+
sqrt(2J), error sqrt(2J), 2J, error 2J as (1x4) array
8295
"""
83-
frame = pd.merge(_get_model_arc_betas(meas_input, plane), lin.loc[:, [f"{AMPLITUDE}{plane}", PEAK2PEAK]],
96+
frame = pd.merge(_get_model_arc_betas(meas_input, plane), lin,
8497
how='inner', left_index=True, right_index=True)
85-
amps = (frame.loc[:, f"{AMPLITUDE}{plane}"].to_numpy() if meas_input.accelerator.excitation
86-
else frame.loc[:, PEAK2PEAK].to_numpy() / 2.0)
87-
meansqrt2j = amps / np.sqrt(frame.loc[:, f"{BETA}{plane}"].to_numpy())
88-
mean2j = np.square(amps) / frame.loc[:, f"{BETA}{plane}"].to_numpy()
89-
return np.array([np.mean(meansqrt2j), np.std(meansqrt2j), np.mean(mean2j), np.std(mean2j)])
98+
99+
if meas_input.accelerator.excitation:
100+
amps = frame.loc[:, f"{AMPLITUDE}{plane}"].to_numpy()
101+
err_amps = frame.loc[:, f"{ERR}{AMPLITUDE}{plane}"].to_numpy()
102+
else:
103+
amps = frame.loc[:, PEAK2PEAK].to_numpy() / 2.0
104+
err_amps = frame.loc[:, f"{CLOSED_ORBIT}{RMS}"].to_numpy()
105+
106+
# sqrt(2J) ---
107+
sqrt_beta = np.sqrt(frame.loc[:, f"{BETA}{plane}"].to_numpy())
108+
109+
actions_sqrt2j = amps / sqrt_beta
110+
errors_sqrt2j = err_amps / sqrt_beta
111+
112+
mean_sqrt2j = weighted_mean(data=actions_sqrt2j, errors=errors_sqrt2j)
113+
err_sqrt2j = weighted_error(data=actions_sqrt2j, errors=errors_sqrt2j)
114+
115+
# 2J ---
116+
actions_2j = np.square(amps) / frame.loc[:, f"{BETA}{plane}"].to_numpy()
117+
errors_2j = 2 * amps * err_amps / frame.loc[:, f"{BETA}{plane}"].to_numpy()
118+
119+
mean_2j = weighted_mean(data=actions_2j, errors=errors_2j)
120+
err_2j = weighted_error(data=actions_2j, errors=errors_2j)
121+
122+
return np.array([mean_sqrt2j, err_sqrt2j, mean_2j, err_2j])
90123

91124

92125
def _get_model_arc_betas(measure_input, plane):

omc3/optics_measurements/measure_optics.py

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import sys
1111
from collections import OrderedDict
1212
from copy import deepcopy
13+
from typing import Dict, List
1314

1415
import numpy as np
1516
import pandas as pd
@@ -21,7 +22,9 @@
2122
chromatic, dispersion, dpp, iforest,
2223
interaction_point, kick, phase, rdt,
2324
tune, crdt, coupling)
24-
from omc3.optics_measurements.constants import (CHROM_BETA_NAME, ERR, EXT)
25+
from omc3.optics_measurements.constants import (
26+
CHROM_BETA_NAME, ERR, EXT, AMPLITUDE, ERR_CALIBRATION, CALIBRATION, CALIBRATION_FILE, NAME, BPM_RESOLUTION
27+
)
2528
from omc3.utils import iotools, logging_tools
2629

2730
LOGGER = logging_tools.get_logger(__name__, level_console=logging_tools.INFO)
@@ -47,7 +50,7 @@ def measure_optics(input_files, measure_input):
4750
invariants = {}
4851
phase_dict = {}
4952
for plane in PLANES:
50-
phase_dict[plane], out_dfs = phase.calculate(measure_input, input_files, tune_dict, plane)
53+
phase_dict[plane], out_dfs = phase.calculate(measure_input, input_files, tune_dict, plane)
5154
phase.write(out_dfs, [common_header]*4, measure_input.outputdir, plane)
5255
phase.write_special(measure_input, phase_dict[plane]['free'], tune_dict[plane]["QF"], plane)
5356
if measure_input.only_coupling:
@@ -150,7 +153,8 @@ def __init__(self, files_to_analyse, optics_opt):
150153
def _repair_backwards_compatible_frame(df, plane):
151154
"""
152155
Multiplies unscaled amplitudes by 2 to get from complex amplitudes to the real ones.
153-
This is for backwards compatibility with Drive.
156+
This is for backwards compatibility with Drive,
157+
i.e. harpy has this
154158
"""
155159
df[f"AMP{plane}"] = df.loc[:, f"AMP{plane}"].to_numpy() * 2
156160
if f"NATAMP{plane}" in df.columns:
@@ -220,16 +224,34 @@ def bpms(self, plane=None, dpp_value=None):
220224
indices[0] = indices[0].intersection(ind)
221225
return indices[0]
222226

223-
def calibrate(self, calibs):
227+
def calibrate(self, calibs: Dict[str, pd.DataFrame]):
228+
"""
229+
Use calibration data to rescale amplitude and amplitude error.
230+
231+
Args:
232+
calibs (Dict): Plane-Dictionary with DataFrames of calibration data.
233+
234+
"""
224235
if calibs is None:
225236
return
237+
226238
for plane in PLANES:
239+
bpm_resolution = calibs[plane].headers.get(BPM_RESOLUTION, 1e-4) # TODO: Default of 0.1 mm is LHC specific
227240
for i in range(len(self[plane])):
228-
data = pd.merge(self[plane][i].loc[:, ["AMP" + plane]], calibs[plane], how='left',
229-
left_index=True, right_index=True).fillna(
230-
value={"CALIBRATION": 1., "ERROR_CALIBRATION": 0.})
231-
self[plane][i][f"AMP{plane}"] = self[plane][i].loc[:, f"AMP{plane}"] * data.loc[:, "CALIBRATION"]
232-
self[plane][i][f"{ERR}AMP{plane}"] = data.loc[:, "ERROR_CALIBRATION"] # TODO
241+
# Merge all measurement BPMs into calibration data (only few BPMs),
242+
# fill missing values with a scaling of 1 and estimated error of 0.1mm (emaclean estimate)
243+
data = pd.merge(self[plane][i].loc[:, [f"{AMPLITUDE}{plane}"]], calibs[plane],
244+
how='left', left_index=True, right_index=True).fillna(
245+
value={CALIBRATION: 1.}) # ERR_CALIBRATION is relative, NaN filled with absolute value below
246+
247+
# Scale amplitude with the calibration
248+
self[plane][i][f"AMP{plane}"] = self[plane][i].loc[:, f"AMP{plane}"] * data.loc[:, CALIBRATION]
249+
250+
# Sum Amplitude Error (absolute) and Calibration Error (relative)
251+
self[plane][i][f"{ERR}{AMPLITUDE}{plane}"] = np.sqrt(
252+
self[plane][i][f"{ERR}{AMPLITUDE}{plane}"]**2 +
253+
((self[plane][i][f"{AMPLITUDE}{plane}"] * data.loc[:, ERR_CALIBRATION]).fillna(bpm_resolution))**2
254+
)
233255

234256
@ staticmethod
235257
def get_columns(frame, column):
@@ -268,7 +290,7 @@ def copy_calibration_files(outputdir, calibrationdir):
268290
return None
269291
calibs = {}
270292
for plane in PLANES:
271-
cal_file = f"calibration_{plane.lower()}.out"
293+
cal_file = CALIBRATION_FILE.format(plane=plane.lower())
272294
iotools.copy_item(os.path.join(calibrationdir, cal_file), os.path.join(outputdir, cal_file))
273-
calibs[plane] = tfs.read(os.path.join(outputdir, cal_file)).set_index("NAME")
295+
calibs[plane] = tfs.read(os.path.join(outputdir, cal_file)).set_index(NAME)
274296
return calibs

omc3/scripts/betabeatsrc_output_converter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""
22
BetaBeat.src Output Converter
3-
-------------
3+
-----------------------------
44
55
Script to convert most important output files produced by ``BetaBeat.src`` / ``GetLLM`` into the standard
66
format used in ``omc3`` to allow straight forward comparison of the two.

0 commit comments

Comments
 (0)