Commit e18c2a06cfe988b28605caef20621daba795dbaa
Committed by
Jürgen Knödlseder
1 parent
fd7ab6ac
add _sim_evt_excess
- add `_set_src_prefactor` function, which sets the test prefactor while checking that the limits are not violated - add safety check as part of `_predict_flux`, to avoid division by zero - add `_sim_evt_excess` function, which is used to increase the test source flux in case the optional conditions on the number of source counts are not met
Showing
2 changed files
with
228 additions
and
26 deletions
cscripts/cssens.par
| ... | ... | @@ -28,19 +28,22 @@ outfile, f, a, sensitivity.fits,,, "Output sensitivity FITS file" |
| 28 | 28 | # |
| 29 | 29 | # Script parameters |
| 30 | 30 | #================== |
| 31 | -offset, r, h, 0.0,,, "Source offset angle (deg)" | |
| 32 | -duration, r, a, 180000.0,,, "Effective exposure time (s)" | |
| 33 | -rad, r, a, 5.0,,, "Radius of ROI (deg)" | |
| 34 | -emin, r, a, 0.03163,,, "Lower energy limit (TeV)" | |
| 35 | -emax, r, a, 125.8925,,, "Upper energy limit (TeV)" | |
| 36 | -bins, i, a, 18,,, "Number of energy bins for sensitivity computation" | |
| 37 | -enumbins, i, h, 0,,, "Number of energy bins for binned analysis (0=unbinned)" | |
| 38 | -npix, i, h, 200,,, "Number of pixels for binned analysis" | |
| 39 | -binsz, r, h, 0.05,,, "Pixel size for binned analysis (deg/pixel)" | |
| 40 | -type, s, h, Differential,Differential|Integral,, "Sensitivity type" | |
| 41 | -sigma, r, h, 5.0,,, "Significance threshold (Gaussian sigma)" | |
| 42 | -max_iter, i, h, 50,,, "Maximum number of iterations" | |
| 43 | -statistic, s, h, DEFAULT,DEFAULT|CSTAT|WSTAT|CHI2,, "Optimization statistic" | |
| 31 | +offset, r, h, 0.0,,, "Source offset angle (deg)" | |
| 32 | +duration, r, a, 180000.0,,, "Effective exposure time (s)" | |
| 33 | +rad, r, a, 5.0,,, "Radius of ROI (deg)" | |
| 34 | +emin, r, a, 0.03163,,, "Lower energy limit (TeV)" | |
| 35 | +emax, r, a, 125.8925,,, "Upper energy limit (TeV)" | |
| 36 | +bins, i, a, 18,,, "Number of energy bins for sensitivity computation" | |
| 37 | +enumbins, i, h, 0,,, "Number of energy bins for binned analysis (0=unbinned)" | |
| 38 | +npix, i, h, 200,,, "Number of pixels for binned analysis" | |
| 39 | +binsz, r, h, 0.05,,, "Pixel size for binned analysis (deg/pixel)" | |
| 40 | +type, s, h, Differential,Differential|Integral,, "Sensitivity type" | |
| 41 | +sigma, r, h, 5.0,,, "Significance threshold (Gaussian sigma)" | |
| 42 | +max_iter, i, h, 50,,, "Maximum number of iterations" | |
| 43 | +statistic, s, h, DEFAULT,DEFAULT|CSTAT|WSTAT|CHI2,, "Optimization statistic" | |
| 44 | +min_src_events, i, h, -1,,, "minimal number of source counts" | |
| 45 | +non_source_uncert_frac, r, h, -1,,, "fraction of no-source counts serving as minimal threshold for source counts" | |
| 46 | +non_source_uncert_rad, r, h, 0.33,,, "radius for circular region in which to integrate no-source counts for threshold" | |
| 44 | 47 | |
| 45 | 48 | # |
| 46 | 49 | # Standard parameters | ... | ... |
cscripts/cssens.py
| ... | ... | @@ -20,6 +20,7 @@ |
| 20 | 20 | # ========================================================================== |
| 21 | 21 | import sys |
| 22 | 22 | import math |
| 23 | +import statistics | |
| 23 | 24 | import gammalib |
| 24 | 25 | import ctools |
| 25 | 26 | from cscripts import obsutils |
| ... | ... | @@ -418,15 +419,7 @@ class cssens(ctools.csobservation): |
| 418 | 419 | # Write header for iteration into logger |
| 419 | 420 | self._log_header2(gammalib.EXPLICIT, 'Iteration '+str(iterations)) |
| 420 | 421 | |
| 421 | - # Create a copy of the test models, set the normalisation parameter | |
| 422 | - # of the test source in the models, and append the models to the | |
| 423 | - # observation. "crab_prefactor" is the prefactor that corresponds | |
| 424 | - # to a flux of 1 Crab. | |
| 425 | - models = test_model.copy() | |
| 426 | - prefactor = modutils.normalisation_parameter(models[self._srcname]) | |
| 427 | - crab_prefactor = prefactor.value() * crab_unit | |
| 428 | - prefactor.value(crab_prefactor * test_crab_flux) | |
| 429 | - self.obs().models(models) | |
| 422 | + crab_prefactor = self._set_src_prefactor(test_model, crab_unit, test_crab_flux) | |
| 430 | 423 | |
| 431 | 424 | # Simulate events for the models. "sim" holds an observation |
| 432 | 425 | # container with observations containing the simulated events. |
| ... | ... | @@ -560,6 +553,31 @@ class cssens(ctools.csobservation): |
| 560 | 553 | ' Test ended after %d iterations.' % max_iter) |
| 561 | 554 | break |
| 562 | 555 | |
| 556 | + n_bck_evts = None | |
| 557 | + for iterations in range(max_iter): | |
| 558 | + pass_evt_cut, n_bck_evts = self._sim_evt_excess( | |
| 559 | + enumbins=enumbins, | |
| 560 | + binsz=binsz, | |
| 561 | + npix=npix, | |
| 562 | + n_bck_evts=n_bck_evts, | |
| 563 | + ) | |
| 564 | + if pass_evt_cut: | |
| 565 | + break | |
| 566 | + | |
| 567 | + # increase the test flux if the cuts failed | |
| 568 | + correct = 1 + ratio_precision | |
| 569 | + crab_flux = correct * crab_flux | |
| 570 | + photon_flux = correct * photon_flux | |
| 571 | + energy_flux = correct * energy_flux | |
| 572 | + sensitivity = correct * sensitivity | |
| 573 | + | |
| 574 | + _ = self._set_src_prefactor(test_model, crab_unit, crab_flux) | |
| 575 | + | |
| 576 | + # Write results into logger | |
| 577 | + name = 'Iteration %d of source count check' % iterations | |
| 578 | + value = 'Fit=%9.4f mCrab Sens=%e erg/cm2/s' % (crab_flux*1000.0, sensitivity) | |
| 579 | + self._log_value(gammalib.TERSE, name, value) | |
| 580 | + | |
| 563 | 581 | # Write fit results into logger |
| 564 | 582 | self._log_header3(gammalib.TERSE, 'Fit results') |
| 565 | 583 | self._log_value(gammalib.TERSE, 'Photon flux', |
| ... | ... | @@ -593,6 +611,181 @@ class cssens(ctools.csobservation): |
| 593 | 611 | # Return result |
| 594 | 612 | return result |
| 595 | 613 | |
| 614 | + # | |
| 615 | + def _set_src_prefactor(self, test_model, crab_unit, test_crab_flux): | |
| 616 | + """ | |
| 617 | + Create a copy of the test models, set the normalisation parameter | |
| 618 | + of the test source in the models, and append the models to the observation. | |
| 619 | + | |
| 620 | + Parameters | |
| 621 | + ---------- | |
| 622 | + test_model : `~gammalib.GModels` | |
| 623 | + Test source model | |
| 624 | + crab_unit : float | |
| 625 | + Crab unit factor | |
| 626 | + test_crab_flux : float | |
| 627 | + Test flux in Crab units (100 mCrab) | |
| 628 | + | |
| 629 | + Returns | |
| 630 | + ------- | |
| 631 | + crab_prefactor : float | |
| 632 | + the prefactor that corresponds to a flux of 1 Crab. | |
| 633 | + """ | |
| 634 | + | |
| 635 | + models = test_model.copy() | |
| 636 | + prefactor = modutils.normalisation_parameter(models[self._srcname]) | |
| 637 | + crab_prefactor = prefactor.value() * crab_unit | |
| 638 | + | |
| 639 | + val_margin = 0.01 | |
| 640 | + min_pref = prefactor.min() | |
| 641 | + max_pref = prefactor.max() | |
| 642 | + | |
| 643 | + val_now = crab_prefactor * test_crab_flux | |
| 644 | + val_now = max(val_now, min_pref * (1 + val_margin)) | |
| 645 | + val_now = min(val_now, max_pref * (1 - val_margin)) | |
| 646 | + crab_prefactor = val_now / test_crab_flux | |
| 647 | + | |
| 648 | + prefactor.value(val_now) | |
| 649 | + | |
| 650 | + self.obs().models(models) | |
| 651 | + | |
| 652 | + return crab_prefactor | |
| 653 | + | |
| 654 | + def _sim_evt_excess(self, enumbins, binsz, npix, n_bck_evts): | |
| 655 | + """ | |
| 656 | + Return the number of excess events for the source model, compared | |
| 657 | + to all other models | |
| 658 | + | |
| 659 | + Parameters | |
| 660 | + ---------- | |
| 661 | + enumbins : int | |
| 662 | + number of bins for the observation simulation | |
| 663 | + binsz : float | |
| 664 | + bin size for the observation simulation | |
| 665 | + npix : int | |
| 666 | + pixel size for the observation simulation | |
| 667 | + n_bck_evts : int | |
| 668 | + number of non-source counts (given to avoid computing the same quantity multiple times) | |
| 669 | + | |
| 670 | + Returns | |
| 671 | + ------- | |
| 672 | + pass_evt_cut : bool | |
| 673 | + does the source pass the minimal excess criteria | |
| 674 | + n_bck_evts : int | |
| 675 | + number of non-source counts | |
| 676 | + """ | |
| 677 | + | |
| 678 | + # number of times to simulate the observation | |
| 679 | + n_sims = 15 | |
| 680 | + | |
| 681 | + # radius of integration for hte non-source background (not necessarily the | |
| 682 | + # same as self['rad'], to match the current standard for non-ctools analyses) | |
| 683 | + rad_deg = self['non_source_uncert_rad'].real() | |
| 684 | + | |
| 685 | + # minimal number of events for the source | |
| 686 | + min_src_events = self['min_src_events'].integer() | |
| 687 | + # threshold by fraction of non-source counts (5 times the assumed uncertainty | |
| 688 | + # on the background, which is taken as 1% of the non-source counts) | |
| 689 | + non_source_uncert_frac = self['non_source_uncert_frac'].real() | |
| 690 | + | |
| 691 | + # end here if no cuts have been spesified | |
| 692 | + if min_src_events <= 0 and non_source_uncert_frac <= 0: | |
| 693 | + return True | |
| 694 | + | |
| 695 | + models = self.obs().models() | |
| 696 | + | |
| 697 | + src_model = gammalib.GModels() | |
| 698 | + bck_models = gammalib.GModels() | |
| 699 | + for s in range(models.size()): | |
| 700 | + model = models[s] | |
| 701 | + if model.name() == self._srcname: | |
| 702 | + src_model.append(model) | |
| 703 | + else: | |
| 704 | + bck_models.append(model) | |
| 705 | + | |
| 706 | + src_obs = self.obs().copy() | |
| 707 | + src_obs.models(src_model) | |
| 708 | + | |
| 709 | + bck_obs = self.obs().copy() | |
| 710 | + bck_obs.models(bck_models) | |
| 711 | + | |
| 712 | + n_src_evts = 0 | |
| 713 | + for is_src in [True, False]: | |
| 714 | + if not is_src: | |
| 715 | + # if the non-source fraction cut is not applied, no need | |
| 716 | + # to calculate the background rate. | |
| 717 | + # n_bck_evts will be None onlu in the firs iteration, where | |
| 718 | + # we only need to compute the non-source counts once, since the | |
| 719 | + # models dont change with the itterations. | |
| 720 | + if non_source_uncert_frac <= 0 or n_bck_evts is not None: | |
| 721 | + continue | |
| 722 | + | |
| 723 | + obs_now = src_obs if is_src else bck_obs | |
| 724 | + | |
| 725 | + n_sim_evts = [] | |
| 726 | + for n_sim in range(n_sims): | |
| 727 | + seed = n_sim | |
| 728 | + | |
| 729 | + sim_now = obsutils.sim( | |
| 730 | + obs_now, | |
| 731 | + nbins=enumbins, | |
| 732 | + seed=seed, | |
| 733 | + binsz=binsz, | |
| 734 | + npix=npix, | |
| 735 | + log=self._log_clients, | |
| 736 | + debug=self['debug'].boolean(), | |
| 737 | + edisp=self['edisp'].boolean(), | |
| 738 | + nthreads=1 | |
| 739 | + ) | |
| 740 | + | |
| 741 | + # consider all events in the entire RoI for the minimal cource count cut | |
| 742 | + if is_src: | |
| 743 | + n_sim_evts += [len(sim_now[0].events())] | |
| 744 | + # consider only background within the thetaSqr cut for the systematic | |
| 745 | + # check, in accordance with the current CTA convention for Li&Ma calculations | |
| 746 | + else: | |
| 747 | + select = ctools.ctselect(sim_now) | |
| 748 | + select['rad'] = rad_deg | |
| 749 | + select['emin'] = 'INDEF' | |
| 750 | + select['tmin'] = 'INDEF' | |
| 751 | + select.run() | |
| 752 | + | |
| 753 | + obs = select.obs() | |
| 754 | + n_sim_evts += [len(obs[0].events())] | |
| 755 | + | |
| 756 | + if is_src: | |
| 757 | + n_src_evts = statistics.median(n_sim_evts) | |
| 758 | + else: | |
| 759 | + n_bck_evts = statistics.median(n_sim_evts) | |
| 760 | + | |
| 761 | + name = 'number of ' + ('source' if is_src else 'non-source') + ' events' | |
| 762 | + value = ( | |
| 763 | + ','.join([str(i) for i in n_sim_evts]) | |
| 764 | + + ' --> median is: ' + str(n_src_evts if is_src else n_bck_evts) | |
| 765 | + ) | |
| 766 | + self._log_value(gammalib.TERSE, name, value) | |
| 767 | + | |
| 768 | + if n_bck_evts is None: | |
| 769 | + n_bck_evts = 0 | |
| 770 | + | |
| 771 | + has_min_evts = n_src_evts >= min_src_events | |
| 772 | + is_above_bck = n_src_evts >= n_bck_evts * non_source_uncert_frac | |
| 773 | + pass_evt_cut = has_min_evts and is_above_bck | |
| 774 | + | |
| 775 | + name = 'minimal excess threshold' | |
| 776 | + value = ( | |
| 777 | + 'has counts above \"min source counts(' | |
| 778 | + + str(min_src_events) + ' events)\": ' + str(has_min_evts) | |
| 779 | + + ' ; pass background uncertainty threshold(' | |
| 780 | + + str(max(non_source_uncert_frac, 0) * 100) + '% == ' | |
| 781 | + + str(n_bck_evts * non_source_uncert_frac) | |
| 782 | + + ' events): ' + str(is_above_bck) | |
| 783 | + + ' --> pass selection: ' + str(pass_evt_cut) | |
| 784 | + ) | |
| 785 | + self._log_value(gammalib.TERSE, name, value) | |
| 786 | + | |
| 787 | + return pass_evt_cut, n_bck_evts | |
| 788 | + | |
| 596 | 789 | def _predict_flux(self, results, ts): |
| 597 | 790 | """ |
| 598 | 791 | Predict Crab flux for a given TS value |
| ... | ... | @@ -635,9 +828,15 @@ class cssens(ctools.csobservation): |
| 635 | 828 | mean_xy *= norm |
| 636 | 829 | mean_xx *= norm |
| 637 | 830 | mean_yy *= norm |
| 638 | - rxy = (mean_xy - mean_x * mean_y) / \ | |
| 639 | - math.sqrt((mean_xx - mean_x*mean_x) * | |
| 640 | - (mean_yy - mean_y*mean_y)) | |
| 831 | + | |
| 832 | + rxy_norm = (mean_xx - mean_x * mean_x) * (mean_yy - mean_y * mean_y) | |
| 833 | + if rxy_norm < 1e-10: | |
| 834 | + rxy_norm = 1 | |
| 835 | + else: | |
| 836 | + rxy_norm = math.sqrt(rxy_norm) | |
| 837 | + | |
| 838 | + rxy = (mean_xy - mean_x * mean_y) / rxy_norm | |
| 839 | + | |
| 641 | 840 | regcoeff = rxy*rxy |
| 642 | 841 | |
| 643 | 842 | # Compute regression line slope |
| ... | ... | @@ -884,4 +1083,4 @@ if __name__ == '__main__': |
| 884 | 1083 | app = cssens(sys.argv) |
| 885 | 1084 | |
| 886 | 1085 | # Run application |
| 887 | 1086 | - app.execute() |
| 1087 | + app.execute() | |
| 888 | 1088 | \ No newline at end of file | ... | ... |