4 Companion TwoRaySpectrumPropagationLossModel calibration script
6 Copyright (c) 2022 SIGNET Lab, Department of Information Engineering,
9 This program is free software: you can redistribute it and/or modify it under
10 the terms of the GNU General Public License as published by the Free Software
11 Foundation, either version 3 of the License, or (at your option) any later
14 This program is distributed in the hope that it will be useful, but WITHOUT
15 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
18 You should have received a copy of the GNU General Public License along with
19 this program. If not, see <http://www.gnu.org/licenses/>.
22 import argparse
as argp
24 from itertools
import product
25 from pathlib
import Path
31 from matplotlib
import pyplot
as plt
35 parser = argp.ArgumentParser(formatter_class=argp.ArgumentDefaultsHelpFormatter)
37 "--num_search_grid_params",
39 help=
"Number of values for each parameter of the search grids",
42 "--num_refinements", default=1, help=
"Number of refinement local search runs to be carried out"
46 default=
"two-ray-to-three-gpp-splm-calibration.csv",
47 help=
"Filename of the fit reference data, obtained from ns-3",
50 "--fit_out_fname", default=
"two-ray-splm-fitted-params.txt", help=
"Filename of the fit results"
53 "--c_plus_plus_out_fname",
54 default=
"two-ray-cplusplus-fitted-params.txt",
55 help=
"Filename of the fit results, encoded as a C++ data structure to be imported in ns-3",
59 default=
"FiguresTwoRayThreeGppChCalibration/",
60 help=
"Output folder for the fit results figures",
62 parser.add_argument(
"--epsilon", default=1e-7, help=
"Tolerance value for the preliminary tests")
64 "--preliminary_fit_test",
66 help=
"Whether to run preliminary tests which check the correctness of the script functions",
69 "--fit_ftr_to_threegpp",
71 help=
"Whether to run the calibration with respect to the 3GPP reference channel gains",
76 help=
"Whether to output the code for importing the calibration results in ns-3",
81 help=
"Whether to plot a comparison of the reference data ECDFs vs the fitted FTR distributions",
84 args = parser.parse_args()
86 num_search_grid_params = int(args.num_search_grid_params)
88 num_refinements = int(args.num_refinements)
90 ref_data_fname = args.ref_data_fname
92 fit_out_fname = args.fit_out_fname
94 c_plus_plus_out_fname = args.c_plus_plus_out_fname
96 figs_folder = args.figs_folder
98 epsilon = float(args.epsilon)
100 preliminary_fit_test = bool(args.preliminary_fit_test)
102 fit_ftr_to_threegpp = bool(args.fit_ftr_to_threegpp)
104 output_ns3_table = bool(args.output_ns3_table)
106 plot_fit_results = bool(args.plot_fit_results)
109 @contextlib.contextmanager
112 Context manager to patch joblib to report into tqdm progress bar given as argument.
113 Taken from: https://stackoverflow.com/questions/24983493/tracking-progress-of-joblib-parallel-execution
117 class TqdmBatchCompletionCallback(joblib.parallel.BatchCompletionCallBack):
118 def __call__(self, *args, **kwargs):
119 tqdm_object.update(n=self.batch_size)
120 return super().__call__(*args, **kwargs)
122 old_batch_callback = joblib.parallel.BatchCompletionCallBack
123 joblib.parallel.BatchCompletionCallBack = TqdmBatchCompletionCallback
127 joblib.parallel.BatchCompletionCallBack = old_batch_callback
142 def __init__(self, m: float, sigma: float, k: float, delta: float):
143 """! The initializer.
144 @param self: the object pointer
145 @param m: Parameter m for the Gamma variable. Used both as the shape and rate parameters.
146 @param sigma: Parameter sigma. Used as the variance of the amplitudes of the normal diffuse components.
147 @param k: Parameter K. Expresses ratio between dominant specular components and diffuse components.
148 @param delta: Parameter delta [0, 1]. Expresses how similar the amplitudes of the two dominant specular components are.
157 """! The initializer with default values.
158 @param self: the object pointer
162 self.
sigmasigma = 1.0
164 self.
deltadelta = 0.0
167 """! The initializer with default values.
168 @param self: the object pointer
169 @returns A string reporting the value of each of the FTR fading model parameters
172 return f
"m: {self.m}, sigma: {self.sigma}, k: {self.k}, delta: {self.delta}"
176 """! Returns the ECDF for the FTR fading model, for a given parameter grid.
177 @param params: The FTR parameters grid.
178 @param n_samples: The number of samples of the output ECDF
179 @param db: Whether to return the ECDF with the gain expressed in dB
180 @returns The ECDF for the FTR fading model
183 assert params.delta >= 0
and params.delta <= 1.0
186 cmn_sqrt_term = np.sqrt(1 - params.delta**2)
187 v1 = np.sqrt(params.sigma) * np.sqrt(params.k * (1 - cmn_sqrt_term))
188 v2 = np.sqrt(params.sigma) * np.sqrt(params.k * (1 + cmn_sqrt_term))
190 assert abs((v1**2 + v2**2) / (2 * params.sigma) - params.k) < 1e-5
192 assert abs((2 * v1 * v2) / (v1**2 + v2**2) - params.delta) < 1e-4
194 assert v1 == v2 == params.k
196 sqrt_gamma = np.sqrt(np.random.gamma(shape=params.m, scale=1 / params.m, size=n_samples))
199 phi1 = np.random.uniform(low=0, high=1.0, size=n_samples)
200 phi2 = np.random.uniform(low=0, high=1.0, size=n_samples)
203 x = np.random.normal(scale=np.sqrt(params.sigma), size=n_samples)
204 y = np.random.normal(scale=np.sqrt(params.sigma), size=n_samples)
207 compl_phi1 = np.vectorize(complex)(np.cos(phi1), np.sin(phi1))
208 compl_phi2 = np.vectorize(complex)(np.cos(phi2), np.sin(phi2))
209 compl_xy = np.vectorize(complex)(x, y)
211 np.multiply(sqrt_gamma, compl_phi1) * v1
212 + np.multiply(sqrt_gamma, compl_phi2) * v2
217 power = np.square(np.absolute(h))
220 power = 10 * np.log10(power)
222 return np.sort(power)
226 """! Computes the mean of the FTR fading model, given a specific set of parameters.
227 @param params: The FTR fading model parameters.
230 cmn_sqrt_term = np.sqrt(1 - params.delta**2)
231 v1 = np.sqrt(params.sigma) * np.sqrt(params.k * (1 - cmn_sqrt_term))
232 v2 = np.sqrt(params.sigma) * np.sqrt(params.k * (1 + cmn_sqrt_term))
234 mean = v1**2 + v2**2 + 2 * params.sigma
240 """! Computes the mean of the FTR fading model using the formula reported in the corresponding paper,
241 given a specific set of parameters.
242 @param params: The FTR fading model parameters.
245 return 2 * params.sigma * (1 + params.k)
249 """! Computes the Anderson-Darling measure for the specified reference and targets distributions.
250 In particular, the Anderson-Darling measure is defined as:
251 \f$A^2 = -N -S\f$, where \f$S = \sum_{i=1}^N \frac{2i - 1}{N} \left[ ln F(Y_i) + ln F(Y_{N + 1 - i}) \right]\f$.
253 See https://www.itl.nist.gov/div898/handbook/eda/section3/eda35e.htm for further details.
255 @param ref_ecdf: The reference ECDF.
256 @param target_ecdf: The target ECDF we wish to match the reference distribution to.
257 @returns The Anderson-Darling measure for the specified reference and targets distributions.
260 assert len(ref_ecdf) == len(target_ecdf)
263 mult_factors = np.linspace(start=1, stop=n, num=n) * 2 + 1
267 with np.errstate(divide=
"ignore"):
268 log_a_plus_b = np.log(ecdf_values) + np.log(1 - np.flip(ecdf_values))
270 valid_idxs = np.isfinite(log_a_plus_b)
271 A_sq = -np.dot(mult_factors[valid_idxs], log_a_plus_b[valid_idxs])
277 """! Given an ECDF and data points belonging to its domain, returns their associated EDCF value.
278 @param ecdf: The ECDF, represented as a sorted list of samples.
279 @param data_points: A list of data points belonging to the same domain as the samples.
280 @returns The ECDF value of the domain points of the specified ECDF
284 for point
in data_points:
285 idx = np.searchsorted(ecdf, point) / len(ecdf)
286 ecdf_values.append(idx)
288 return np.asarray(ecdf_values)
292 """! Computes the value for the FTR parameter sigma, given k, yielding a unit-mean fading process.
293 @param k: The K parameter of the FTR fading model, which represents the ratio of the average power
294 of the dominant components to the power of the remaining diffuse multipath.
295 @returns The value for the FTR parameter sigma, given k, yielding a unit-mean fading process.
298 return 1 / (2 + 2 * k)
302 ref_data: pd.DataFrame, ref_params_combo: tuple, num_params: int, num_refinements: int
304 """! Estimate the FTR parameters yielding the closest ECDF to the reference one.
306 Uses a global search to estimate the FTR parameters yielding the best fit to the reference ECDF.
307 Then, the search is refined by repeating the procedure in the neighborhood of the parameters
308 identified with the global search. Such a neighborhood is determined as the interval whose center
309 is the previous iteration best value, and the lower and upper bounds are the first lower and upper
310 values which were previously considered, respectively.
312 @param ref_data: The reference data, represented as a DataFrame of samples.
313 @param ref_params_combo: The specific combination of simulation parameters corresponding
314 to the reference ECDF
315 @param num_params: The number of values of each parameter in the global and local search grids.
316 @param num_refinements: The number of local refinement search to be carried out after the global search.
318 @returns An estimate of the FTR parameters yielding the closest ECDF to the reference one.
322 ref_ecdf = ref_data.query(
323 "scen == @ref_params_combo[0] and cond == @ref_params_combo[1] and fc == @ref_params_combo[2]"
327 n_samples = len(ref_ecdf)
334 m_and_k_step = (m_and_k_ub - m_and_k_lb) / n_samples
337 delta_step = 1 / n_samples
340 coarse_search_grid = {
343 np.ones(num_params) * 10,
344 np.linspace(start=m_and_k_lb, stop=m_and_k_ub, endpoint=
True, num=num_params),
348 np.ones(num_params) * 10,
349 np.linspace(start=m_and_k_lb, stop=m_and_k_ub, endpoint=
True, num=num_params),
352 "delta": np.linspace(start=0.0, stop=1.0, endpoint=
True, num=num_params),
356 for element
in product(*coarse_search_grid.values()):
359 params.m = element[0]
360 params.k = element[1]
361 params.delta = element[2]
368 if ad_meas < best_ad:
372 for _
in range(num_refinements):
374 finer_search_grid = {
376 np.ones(num_params) * 10,
378 start=
max(0, np.log10(best_params.m) - m_and_k_step),
379 stop=np.log10(best_params.m) + m_and_k_step,
385 np.ones(num_params) * 10,
387 start=
max(0, np.log10(best_params.k) - m_and_k_step),
388 stop=np.log10(best_params.k) + m_and_k_step,
393 "delta": np.linspace(
394 start=
max(0, best_params.delta - delta_step),
395 stop=
min(1, best_params.delta + delta_step),
403 np.log10(best_params.m) + m_and_k_step -
max(0, np.log10(best_params.m) - m_and_k_step)
406 min(1, best_params.delta + 1 / num_params) -
max(0, best_params.delta - 1 / num_params)
409 for element
in product(*finer_search_grid.values()):
412 params.m = element[0]
413 params.k = element[1]
414 params.delta = element[2]
421 if ad_meas < best_ad:
426 f
"{ref_params_combo[0]}\t{ref_params_combo[1]}\t{ref_params_combo[2]}"
427 + f
" \t{best_params.sigma}\t{best_params.k}\t{best_params.delta}\t{best_params.m}\n"
434 text += f
"TwoRaySpectrumPropagationLossModel::FtrParams({np.format_float_scientific(params.m)}, {np.format_float_scientific(params.sigma)}, \
435 {np.format_float_scientific(params.k)}, {np.format_float_scientific(params.delta)})"
442 Prints to a file the results of the FTR fit, as C++ code.
445 fit (pd.DataFrame): A Pandas Dataframe holding the results of the FTR fit.
446 out_fname (str): The name of the file to print the C++ code to.
451 for scen
in set(fit[
"scen"]):
452 out_str += f
'{{"{scen}",\n{{'
454 for cond
in set(fit[
"cond"]):
455 out_str += f
"{{ChannelCondition::LosConditionValue::{cond}, \n"
458 freqs = np.sort(
list(set(fit[
"fc"])))
461 out_str += f
"{float(fc)}, "
462 out_str = out_str[0:-2]
467 fit_line = fit.query(
"scen == @scen and cond == @cond and fc == @fc")
468 assert fit_line.reset_index().shape[0] == 1
471 params.m = fit_line.iloc[0][
"m"]
472 params.k = fit_line.iloc[0][
"k"]
473 params.delta = fit_line.iloc[0][
"delta"]
474 params.sigma = fit_line.iloc[0][
"sigma"]
480 out_str = out_str[0:-2]
484 out_str = out_str[0:-2]
487 out_str = out_str[0:-2]
490 with open(out_fname,
"w", encoding=
"utf-8")
as f:
494 if __name__ ==
"__main__":
500 df = pd.read_csv(ref_data_fname, sep=
"\t")
502 df[
"gain"] = 10 * np.log10(df[
"gain"])
505 scenarios = set(df[
"scen"])
506 is_los = set(df[
"cond"])
507 frequencies = np.sort(
list(set(df[
"fc"])))
513 if preliminary_fit_test:
520 for delta
in np.linspace(start=0, stop=1, num=100):
524 avg_mean = np.mean(mean_list)
525 assert np.all(np.abs(mean_list - avg_mean) < epsilon)
531 for k
in np.linspace(start=1, stop=500, num=50):
538 assert np.all(np.abs(mean_list - np.float64(1.0)) < epsilon)
539 assert np.all(np.abs(mean_th_list - np.float64(1.0)) < epsilon)
541 if fit_ftr_to_threegpp:
545 desc=
"Fitting FTR to the 3GPP fading model",
546 total=(len(scenarios) * len(is_los) * len(frequencies)),
549 res = joblib.Parallel(n_jobs=10)(
550 joblib.delayed(fit_ftr_to_reference)(
551 df, params_comb, num_search_grid_params, num_refinements
553 for params_comb
in product(scenarios, is_los, frequencies)
556 with open(fit_out_fname,
"w", encoding=
"utf-8")
as f:
557 f.write(
"scen\tcond\tfc\tsigma\tk\tdelta\tm\n")
563 fit = pd.read_csv(fit_out_fname, delimiter=
"\t")
570 sns.set(rc={
"figure.figsize": (7, 5)})
572 sns.set_style(
"darkgrid")
574 fit = pd.read_csv(fit_out_fname, delimiter=
"\t")
576 Path(figs_folder).mkdir(parents=
True, exist_ok=
True)
579 for params_comb
in product(scenarios, is_los, frequencies):
581 "scen == @params_comb[0] and cond == @params_comb[1] and fc == @params_comb[2]"
585 ref_data = df.query(data_query)
588 fit_line = fit.query(data_query)
589 assert fit_line.reset_index().shape[0] == 1
591 params.m = fit_line.iloc[0][
"m"]
592 params.k = fit_line.iloc[0][
"k"]
593 params.delta = fit_line.iloc[0][
"delta"]
594 params.sigma = fit_line.iloc[0][
"sigma"]
601 ad_measures.append(np.sqrt(ad_meas))
603 sns.ecdfplot(data=ref_data, x=
"gain", label=
"38.901 reference model")
604 sns.ecdfplot(ftr_ecdf, label=f
"Fitted FTR, sqrt(AD)={round(np.sqrt(ad_meas), 2)}")
605 plt.xlabel(
"End-to-end channel gain due to small scale fading [dB]")
608 f
"{figs_folder}{params_comb[0]}_{params_comb[1]}_{params_comb[2]/1e9}GHz_fit.png",
615 sns.ecdfplot(ad_measures, label=
"AD measures")
616 plt.xlabel(
"Anderson-Darling goodness-of-fit")
618 plt.savefig(f
"{figs_folder}AD_measures.png", dpi=500, bbox_inches=
"tight")
def __str__(self)
The initializer with default values.
def __init__(self, float m, float sigma, float k, float delta)
The initializer.
delta
Parameter delta [0, 1].
m
Parameter m for the Gamma variable.
str append_ftr_params_to_cpp_string(str text, FtrParams params)
float compute_anderson_darling_measure(list ref_ecdf, list target_ecdf)
Computes the Anderson-Darling measure for the specified reference and targets distributions.
def compute_ftr_th_mean(FtrParams params)
Computes the mean of the FTR fading model using the formula reported in the corresponding paper,...
str fit_ftr_to_reference(pd.DataFrame ref_data, tuple ref_params_combo, int num_params, int num_refinements)
Estimate the FTR parameters yielding the closest ECDF to the reference one.
def get_ftr_ecdf(FtrParams params, int n_samples, db=False)
Returns the ECDF for the FTR fading model, for a given parameter grid.
def print_cplusplus_map_from_fit_results(pd.DataFrame fit, str out_fname)
float get_sigma_from_k(float k)
Computes the value for the FTR parameter sigma, given k, yielding a unit-mean fading process.
def compute_ftr_mean(FtrParams params)
Computes the mean of the FTR fading model, given a specific set of parameters.
def tqdm_joblib(tqdm_object)
np.ndarray compute_ecdf_value(list ecdf, float data_points)
Given an ECDF and data points belonging to its domain, returns their associated EDCF value.