Coverage for pesummary/cli/summaryrecreate.py: 83.1%

142 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-05-02 08:42 +0000

1#! /usr/bin/env python 

2 

3# Licensed under an MIT style license -- see LICENSE.md 

4 

5from pesummary.core.cli.parser import ArgumentParser as _ArgumentParser 

6from pesummary.core.cli.actions import DelimiterSplitAction 

7from pesummary.gw.file.read import read 

8from pesummary.utils.utils import logger, make_dir 

9from pesummary.utils.dict import edit_dictionary, paths_to_key 

10from pesummary.utils.exceptions import InputError 

11from pesummary.io import write 

12 

13import subprocess 

14import os 

15import copy 

16 

17__author__ = ["Charlie Hoy <charlie.hoy@ligo.org>"] 

18__doc__ = """This executable is used to recreate the analysis which was used 

19to generate the posterior samples stored in the metafile""" 

20 

21 

22def launch_subprocess(command_line): 

23 """Run a command line 

24 

25 Parameters 

26 ---------- 

27 command_line: str 

28 the command line you wish to run 

29 """ 

30 logger.info("Running '{}'".format(command_line)) 

31 process = subprocess.Popen(command_line, shell=True) 

32 process.wait() 

33 

34 

35class ArgumentParser(_ArgumentParser): 

36 def _pesummary_options(self): 

37 options = super(ArgumentParser, self)._pesummary_options() 

38 options.update( 

39 { 

40 "--samples": { 

41 "short": "-s", 

42 "required": True, 

43 "help": ( 

44 "Path to the PESummary file which stores the analysis " 

45 "you wish to recreate." 

46 ) 

47 }, 

48 "--rundir": { 

49 "default": "./", 

50 "help": "The run directory of the analysis", 

51 "short": "-r", 

52 }, 

53 "--code": { 

54 "default": "lalinference", 

55 "short": "-c", 

56 "help": "The sampling software you wish to run" 

57 }, 

58 "--delimiter": { 

59 "default": ":", 

60 "help": ( 

61 "Delimiter used to seperate the existing and new " 

62 "quantity" 

63 ) 

64 }, 

65 "--config_override": { 

66 "action": DelimiterSplitAction, 

67 "nargs": "+", 

68 "default": {}, 

69 "help": ( 

70 "Changes you wish to make to the configuration file. " 

71 "Must be in the form `key:item` where key is the entry " 

72 "in the config file you wish to modify, ':' is the " 

73 "default delimiter, and item is the string you wish to " 

74 "replace with." 

75 ) 

76 }, 

77 } 

78 ) 

79 return options 

80 

81 

82class LALInference(object): 

83 """Class to create a LALInference analysis 

84 """ 

85 @staticmethod 

86 def pipe(rundir, config, **kwargs): 

87 """Launch the `lalinference_pipe` executable 

88 

89 Parameters 

90 ---------- 

91 rundir: str 

92 path to the run directory of the analysis 

93 config: str 

94 path to the configuration file 

95 kwargs: dict 

96 dictionary of command line arguments that are passed directly to 

97 `lalinference_pipe` 

98 """ 

99 command_line = "lalinference_pipe {} -r {}".format( 

100 config, os.path.join(rundir, "outdir") 

101 ) 

102 for key, item in kwargs.items(): 

103 command_line += " --{} {}".format(key, item) 

104 launch_subprocess(command_line) 

105 

106 @staticmethod 

107 def map(parameter, delimiter=":"): 

108 """Map a parameter name to LALInference format 

109 

110 Parameters 

111 ---------- 

112 parameter: str 

113 The parameter you wish to map 

114 delimiter: str, optional 

115 The delimiter to split the parameter 

116 """ 

117 not_equal_to_length_two_error = ( 

118 "The {} must be in the form '{}:{}' where '%s' is the default " 

119 "delimiter" % (delimiter) 

120 ) 

121 p = parameter.split(delimiter) 

122 if "psd" in p: 

123 if len(p) != 2: 

124 raise ValueError( 

125 not_equal_to_length_two_error.format("psd", "ifo", "psd") 

126 ) 

127 return "{}-{}".format(p[0].lower(), p[1]) 

128 elif "calibration" in p: 

129 if len(p) != 2: 

130 raise ValueError( 

131 not_equal_to_length_two_error.format( 

132 "calibration", "ifo", "calibration" 

133 ) 

134 ) 

135 return "{}-spcal-envelope".format(p[0].lower()) 

136 else: 

137 return delimiter.join(p) 

138 

139 

140class Code(object): 

141 lalinference = LALInference 

142 

143 

144class _Input(object): 

145 """Super class to handle the command line arguments 

146 """ 

147 @property 

148 def samples(self): 

149 return self._samples 

150 

151 @samples.setter 

152 def samples(self, samples): 

153 if not os.path.isfile(samples): 

154 raise FileNotFoundError( 

155 "File '{}' does not exist".format(samples) 

156 ) 

157 self._samples = read(samples) 

158 

159 @property 

160 def labels(self): 

161 return self._labels 

162 

163 @labels.setter 

164 def labels(self, labels): 

165 self._labels = self.samples.labels 

166 if labels is not None: 

167 self._labels = labels 

168 for label in labels: 

169 if label not in self.samples.labels: 

170 raise InputError( 

171 "The label '{}' is not stored in the result file. " 

172 "The list of available labels is {}".format( 

173 label, ", ".join(self.samples.labels) 

174 ) 

175 ) 

176 logger.info( 

177 "Recreating the following analyses: '{}'".format( 

178 ", ".join(labels) 

179 ) 

180 ) 

181 else: 

182 logger.info( 

183 "No labels provided. Recreating all analyses stored in the " 

184 "result file: {}".format(", ".join(self.samples.labels)) 

185 ) 

186 

187 @property 

188 def rundir(self): 

189 return self._rundir 

190 

191 @rundir.setter 

192 def rundir(self, rundir): 

193 self._rundir = os.path.abspath(rundir) 

194 make_dir(rundir) 

195 logger.info( 

196 "Setting '{}' to be the run directory of the analysis".format( 

197 rundir 

198 ) 

199 ) 

200 for label in self.labels: 

201 make_dir(os.path.join(rundir, label)) 

202 

203 @property 

204 def code(self): 

205 return self._code 

206 

207 @code.setter 

208 def code(self, code): 

209 allowed_codes = ["lalinference"] 

210 if code.lower() not in allowed_codes: 

211 raise InputError( 

212 "The 'summaryrecreate' executable is currently only configured " 

213 "to run with the following codes: {}".format( 

214 ", ".join(allowed_codes) 

215 ) 

216 ) 

217 self._code = getattr(Code, code.lower()) 

218 

219 def _write_to_file(self, attribute): 

220 """Write an attribute stored in the result file to file 

221 

222 Parameters 

223 ---------- 

224 attribute: str 

225 name of the attribute that you wish to write to file 

226 """ 

227 f = self.samples 

228 data = getattr(f, attribute) 

229 if len(data) and all(data[label] != {} for label in self.labels): 

230 for label in self.labels: 

231 for ifo in data[label].keys(): 

232 filename = os.path.join( 

233 self.rundir, label, "{}_{}.dat".format(ifo, attribute) 

234 ) 

235 data[label][ifo].save_to_file(filename) 

236 name = self.code.map( 

237 ":".join([ifo, attribute]), delimiter=":" 

238 ) 

239 self.settings_to_change[label][name] = filename 

240 else: 

241 logger.warning( 

242 "No {} data found in the file. Using the {}s stored in the " 

243 "configuration file".format(attribute, attribute) 

244 ) 

245 

246 def write_psd_to_file(self): 

247 """Write the psd to file 

248 """ 

249 self._write_to_file("psd") 

250 

251 def write_calibration_to_file(self): 

252 """Write the calibration data to file 

253 """ 

254 self._write_to_file("calibration") 

255 

256 def write_config_file(self): 

257 """Save the config file to the run directory and return the data 

258 stored within 

259 """ 

260 if not len(self.samples.config): 

261 raise ValueError( 

262 "No configuration file stored in the result file. Unable to " 

263 "recreate the analysis" 

264 ) 

265 config_files = {} 

266 for label in self.labels: 

267 config_data = copy.deepcopy(self.samples.config[label]) 

268 if self.settings_to_change is not None: 

269 for key, item in self.settings_to_change[label].items(): 

270 try: 

271 path, = paths_to_key(key, config_data) 

272 config_data = edit_dictionary(config_data, path, item) 

273 except ValueError: 

274 logger.warning( 

275 "Unable to change '{}' to '{}' in the config " 

276 "file".format(key, item) 

277 ) 

278 outdir = os.path.join(self.rundir, label) 

279 write( 

280 config_data, outdir=outdir, filename="config.ini", 

281 file_format="ini" 

282 ) 

283 logger.info("Writing the configuration file to: {}".format(outdir)) 

284 config_files[label] = os.path.join(outdir, "config.ini") 

285 return config_files 

286 

287 

288class Input(_Input): 

289 """Class to handle the command line arguments 

290 

291 Parameters 

292 ---------- 

293 opts: argparse.Namespace 

294 Namespace object containing the command line options 

295 """ 

296 def __init__(self, opts): 

297 logger.info(opts) 

298 self.opts = opts 

299 self.samples = self.opts.samples 

300 self.labels = self.opts.labels 

301 self.settings_to_change = {label: {} for label in self.labels} 

302 self.rundir = self.opts.rundir 

303 self.code = self.opts.code 

304 self.write_psd_to_file() 

305 self.write_calibration_to_file() 

306 for key, item in self.opts.config_override.items(): 

307 for label in self.labels: 

308 name = self.code.map(key) 

309 self.settings_to_change[label][name] = item 

310 self.config = self.write_config_file() 

311 

312 

313def main(args=None): 

314 """The main function for the `summaryrecreate` executable 

315 """ 

316 parser = ArgumentParser(description=__doc__) 

317 parser.add_known_options_to_parser( 

318 [ 

319 "--samples", "--labels", "--rundir", "--code", "--delimiter", 

320 "--config_override" 

321 ] 

322 ) 

323 opts = parser.parse_args(args=args) 

324 inputs = Input(opts) 

325 for label in inputs.labels: 

326 inputs.code.pipe(os.path.join(inputs.rundir, label), inputs.config[label]) 

327 

328 

329if __name__ == "__main__": 

330 main()