Coverage for src\time_series_analyzer\cli.py: 0%

140 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-23 11:57 +0800

1""" 

2命令行界面 

3 

4使用click框架实现用户友好的命令行工具。 

5""" 

6 

7import click 

8from pathlib import Path 

9from typing import Optional 

10import sys 

11 

12from .parsers import ModelParser 

13from .formatters import OutputFormatter 

14from .transfer_function import TransferFunctionDeriver 

15from .models import ARIMAModel, SeasonalARIMAModel 

16 

17 

18@click.group() 

19@click.version_option(version="0.1.0") 

20def main(): 

21 """ 

22 时序模型传递函数分析器 

23  

24 自动推导ARIMA和SARIMA模型的传递函数表达式。 

25 """ 

26 pass 

27 

28 

29@main.command() 

30@click.option('--model', '-m', 

31 help='模型字符串,如 "ARIMA(2,1,1)" 或 "SARIMA(2,1,1)(1,1,1,12)"') 

32@click.option('--file', '-f', type=click.Path(exists=True), 

33 help='包含模型参数的配置文件 (JSON/YAML)') 

34@click.option('--interactive', '-i', is_flag=True, 

35 help='交互式输入模型参数') 

36@click.option('--output', '-o', type=click.Path(), 

37 help='输出文件路径') 

38@click.option('--format', 'output_format', 

39 type=click.Choice(['text', 'latex', 'json'], case_sensitive=False), 

40 default='text', help='输出格式') 

41@click.option('--include-analysis', is_flag=True, 

42 help='包含稳定性分析') 

43@click.option('--precision', type=int, default=4, 

44 help='数值精度 (小数位数)') 

45def analyze(model: Optional[str], file: Optional[str], interactive: bool, 

46 output: Optional[str], output_format: str, include_analysis: bool, 

47 precision: int): 

48 """ 

49 分析时间序列模型并生成传递函数 

50 """ 

51 try: 

52 # 解析模型 

53 if interactive: 

54 model_obj = ModelParser.parse_interactive() 

55 elif file: 

56 model_obj = ModelParser.parse_from_file(file) 

57 elif model: 

58 model_obj = ModelParser.parse_from_string(model) 

59 else: 

60 click.echo("错误: 必须指定模型参数 (使用 --model, --file 或 --interactive)", err=True) 

61 sys.exit(1) 

62 

63 # 创建格式化器 

64 formatter = OutputFormatter(precision=precision) 

65 

66 # 生成输出 

67 if output_format.lower() == 'latex': 

68 result = formatter.format_latex( 

69 model_obj, 

70 include_transfer_function=True, 

71 include_analysis=include_analysis 

72 ) 

73 elif output_format.lower() == 'json': 

74 result = formatter.format_json( 

75 model_obj, 

76 include_transfer_function=True, 

77 include_analysis=include_analysis 

78 ) 

79 else: # text 

80 result = formatter.format_plain_text( 

81 model_obj, 

82 include_transfer_function=True, 

83 include_analysis=include_analysis 

84 ) 

85 

86 # 输出结果 

87 if output: 

88 with open(output, 'w', encoding='utf-8') as f: 

89 f.write(result) 

90 click.echo(f"结果已保存到: {output}") 

91 else: 

92 click.echo(result) 

93 

94 except Exception as e: 

95 click.echo(f"错误: {e}", err=True) 

96 sys.exit(1) 

97 

98 

99@main.command() 

100@click.option('--model', '-m', required=True, 

101 help='模型字符串,如 "ARIMA(2,1,1)" 或 "SARIMA(2,1,1)(1,1,1,12)"') 

102@click.option('--max-lag', type=int, default=20, 

103 help='最大滞后阶数') 

104@click.option('--output', '-o', type=click.Path(), 

105 help='输出文件路径') 

106@click.option('--format', 'output_format', 

107 type=click.Choice(['text', 'json'], case_sensitive=False), 

108 default='text', help='输出格式') 

109def impulse(model: str, max_lag: int, output: Optional[str], output_format: str): 

110 """ 

111 计算脉冲响应函数 

112 """ 

113 try: 

114 # 解析模型 

115 model_obj = ModelParser.parse_from_string(model) 

116 

117 # 计算脉冲响应 

118 deriver = TransferFunctionDeriver() 

119 impulse_response = deriver.derive_impulse_response(model_obj, max_lag) 

120 

121 # 格式化输出 

122 if output_format.lower() == 'json': 

123 import json 

124 result = json.dumps({ 

125 "model": model_obj.to_dict(), 

126 "impulse_response": {str(k): str(v) for k, v in impulse_response.items()}, 

127 "max_lag": max_lag 

128 }, indent=2, ensure_ascii=False) 

129 else: 

130 lines = [f"{model_obj.name} 脉冲响应函数", "=" * 40] 

131 for lag, coeff in impulse_response.items(): 

132 lines.append(f"h[{lag}] = {coeff}") 

133 result = "\n".join(lines) 

134 

135 # 输出结果 

136 if output: 

137 with open(output, 'w', encoding='utf-8') as f: 

138 f.write(result) 

139 click.echo(f"脉冲响应已保存到: {output}") 

140 else: 

141 click.echo(result) 

142 

143 except Exception as e: 

144 click.echo(f"错误: {e}", err=True) 

145 sys.exit(1) 

146 

147 

148@main.command() 

149@click.option('--model', '-m', required=True, 

150 help='模型字符串,如 "ARIMA(2,1,1)" 或 "SARIMA(2,1,1)(1,1,1,12)"') 

151@click.option('--frequencies', type=str, default="0,0.1,0.2,0.3,0.4,0.5", 

152 help='频率列表 (逗号分隔)') 

153@click.option('--output', '-o', type=click.Path(), 

154 help='输出文件路径') 

155@click.option('--format', 'output_format', 

156 type=click.Choice(['text', 'json'], case_sensitive=False), 

157 default='text', help='输出格式') 

158def frequency(model: str, frequencies: str, output: Optional[str], output_format: str): 

159 """ 

160 计算频率响应 

161 """ 

162 try: 

163 # 解析模型 

164 model_obj = ModelParser.parse_from_string(model) 

165 

166 # 解析频率 

167 freq_list = [float(f.strip()) for f in frequencies.split(',')] 

168 

169 # 计算频率响应 

170 deriver = TransferFunctionDeriver() 

171 freq_response = deriver.get_frequency_response(model_obj, freq_list) 

172 

173 # 格式化输出 

174 if output_format.lower() == 'json': 

175 import json 

176 result = json.dumps({ 

177 "model": model_obj.to_dict(), 

178 "frequency_response": { 

179 "frequencies": freq_response["frequencies"], 

180 "magnitudes": [float(m) for m in freq_response["magnitudes"]], 

181 "phases": [float(p) for p in freq_response["phases"]], 

182 "magnitude_db": [float(m) for m in freq_response["magnitude_db"]] 

183 } 

184 }, indent=2, ensure_ascii=False) 

185 else: 

186 lines = [f"{model_obj.name} 频率响应", "=" * 40] 

187 lines.append(f"{'频率':<10} {'幅度':<15} {'相位':<15} {'幅度(dB)':<15}") 

188 lines.append("-" * 60) 

189 

190 for i, freq in enumerate(freq_response["frequencies"]): 

191 mag = freq_response["magnitudes"][i] 

192 phase = freq_response["phases"][i] 

193 mag_db = freq_response["magnitude_db"][i] 

194 lines.append(f"{freq:<10.3f} {mag:<15.6f} {phase:<15.6f} {mag_db:<15.3f}") 

195 

196 result = "\n".join(lines) 

197 

198 # 输出结果 

199 if output: 

200 with open(output, 'w', encoding='utf-8') as f: 

201 f.write(result) 

202 click.echo(f"频率响应已保存到: {output}") 

203 else: 

204 click.echo(result) 

205 

206 except Exception as e: 

207 click.echo(f"错误: {e}", err=True) 

208 sys.exit(1) 

209 

210 

211@main.command() 

212@click.option('--model', '-m', required=True, 

213 help='模型字符串,如 "ARIMA(2,1,1)" 或 "SARIMA(2,1,1)(1,1,1,12)"') 

214@click.option('--output', '-o', type=click.Path(), 

215 help='输出文件路径') 

216def stability(model: str, output: Optional[str]): 

217 """ 

218 分析模型稳定性 

219 """ 

220 try: 

221 # 解析模型 

222 model_obj = ModelParser.parse_from_string(model) 

223 

224 # 稳定性分析 

225 deriver = TransferFunctionDeriver() 

226 stability_result = deriver.analyze_stability(model_obj) 

227 

228 # 格式化输出 

229 lines = [f"{model_obj.name} 稳定性分析", "=" * 40] 

230 lines.append(f"系统稳定性: {'稳定' if stability_result['is_stable'] else '不稳定'}") 

231 lines.append(f"最大极点模长: {stability_result['max_pole_magnitude']:.6f}") 

232 lines.append(f"稳定性裕度: {stability_result['stability_margin']:.6f}") 

233 lines.append("") 

234 

235 if stability_result['poles']: 

236 lines.append("极点:") 

237 for i, pole in enumerate(stability_result['poles']): 

238 lines.append(f" p_{i+1} = {pole:.6f} (|p_{i+1}| = {abs(pole):.6f})") 

239 

240 if stability_result['zeros']: 

241 lines.append("") 

242 lines.append("零点:") 

243 for i, zero in enumerate(stability_result['zeros']): 

244 lines.append(f" z_{i+1} = {zero:.6f}") 

245 

246 if not stability_result['is_stable']: 

247 lines.append("") 

248 lines.append("警告: 系统不稳定!存在模长大于等于1的极点。") 

249 

250 result = "\n".join(lines) 

251 

252 # 输出结果 

253 if output: 

254 with open(output, 'w', encoding='utf-8') as f: 

255 f.write(result) 

256 click.echo(f"稳定性分析已保存到: {output}") 

257 else: 

258 click.echo(result) 

259 

260 except Exception as e: 

261 click.echo(f"错误: {e}", err=True) 

262 sys.exit(1) 

263 

264 

265@main.command() 

266def examples(): 

267 """ 

268 显示使用示例 

269 """ 

270 examples_text = """ 

271使用示例: 

272 

2731. 分析简单ARIMA模型: 

274 tsm-analyzer analyze -m "ARIMA(2,1,1)" 

275 

2762. 分析带参数的ARIMA模型: 

277 tsm-analyzer analyze -m "ARIMA(2,1,1)" --include-analysis 

278 

2793. 分析SARIMA模型并输出LaTeX格式: 

280 tsm-analyzer analyze -m "SARIMA(2,1,1)(1,1,1,12)" --format latex 

281 

2824. 从配置文件分析: 

283 tsm-analyzer analyze -f model_config.json --format json -o result.json 

284 

2855. 交互式输入: 

286 tsm-analyzer analyze --interactive 

287 

2886. 计算脉冲响应: 

289 tsm-analyzer impulse -m "ARIMA(2,1,1)" --max-lag 10 

290 

2917. 计算频率响应: 

292 tsm-analyzer frequency -m "ARIMA(2,1,1)" --frequencies "0,0.1,0.2,0.3,0.4,0.5" 

293 

2948. 稳定性分析: 

295 tsm-analyzer stability -m "ARIMA(2,1,1)" 

296 

297配置文件格式 (JSON): 

298{ 

299 "model_type": "ARIMA", 

300 "p": 2, 

301 "d": 1, 

302 "q": 1, 

303 "ar_params": [0.5, -0.3], 

304 "ma_params": [0.2], 

305 "constant": 0 

306} 

307 

308配置文件格式 (YAML): 

309model_type: SARIMA 

310p: 2 

311d: 1 

312q: 1 

313P: 1 

314D: 1 

315Q: 1 

316m: 12 

317ar_params: [0.5, -0.3] 

318ma_params: [0.2] 

319seasonal_ar_params: [0.8] 

320seasonal_ma_params: [0.4] 

321""" 

322 click.echo(examples_text) 

323 

324 

325if __name__ == '__main__': 

326 main()