pyfemtet 0.3.12__py3-none-any.whl → 0.4.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pyfemtet might be problematic. Click here for more details.

Files changed (35) hide show
  1. pyfemtet/FemtetPJTSample/NX_ex01/NX_ex01.py +61 -32
  2. pyfemtet/FemtetPJTSample/Sldworks_ex01/Sldworks_ex01.py +62 -40
  3. pyfemtet/FemtetPJTSample/gau_ex08_parametric.py +26 -23
  4. pyfemtet/FemtetPJTSample/her_ex40_parametric.femprj +0 -0
  5. pyfemtet/FemtetPJTSample/her_ex40_parametric.py +58 -46
  6. pyfemtet/FemtetPJTSample/wat_ex14_parallel_parametric.py +31 -29
  7. pyfemtet/FemtetPJTSample/wat_ex14_parametric.femprj +0 -0
  8. pyfemtet/FemtetPJTSample/wat_ex14_parametric.py +30 -28
  9. pyfemtet/__init__.py +1 -1
  10. pyfemtet/core.py +14 -0
  11. pyfemtet/dispatch_extensions.py +5 -0
  12. pyfemtet/opt/__init__.py +22 -2
  13. pyfemtet/opt/_femopt.py +544 -0
  14. pyfemtet/opt/_femopt_core.py +732 -0
  15. pyfemtet/opt/interface/__init__.py +15 -0
  16. pyfemtet/opt/interface/_base.py +71 -0
  17. pyfemtet/opt/{interface.py → interface/_femtet.py} +121 -408
  18. pyfemtet/opt/interface/_femtet_with_nx/__init__.py +3 -0
  19. pyfemtet/opt/interface/_femtet_with_nx/_interface.py +128 -0
  20. pyfemtet/opt/interface/_femtet_with_sldworks.py +174 -0
  21. pyfemtet/opt/opt/__init__.py +8 -0
  22. pyfemtet/opt/opt/_base.py +202 -0
  23. pyfemtet/opt/opt/_optuna.py +246 -0
  24. pyfemtet/opt/visualization/__init__.py +7 -0
  25. pyfemtet/opt/visualization/_graphs.py +222 -0
  26. pyfemtet/opt/visualization/_monitor.py +1149 -0
  27. {pyfemtet-0.3.12.dist-info → pyfemtet-0.4.2.dist-info}/METADATA +6 -5
  28. pyfemtet-0.4.2.dist-info/RECORD +38 -0
  29. {pyfemtet-0.3.12.dist-info → pyfemtet-0.4.2.dist-info}/WHEEL +1 -1
  30. pyfemtet-0.4.2.dist-info/entry_points.txt +3 -0
  31. pyfemtet/opt/base.py +0 -1490
  32. pyfemtet/opt/monitor.py +0 -474
  33. pyfemtet-0.3.12.dist-info/RECORD +0 -26
  34. /pyfemtet/opt/{_FemtetWithNX → interface/_femtet_with_nx}/update_model.py +0 -0
  35. {pyfemtet-0.3.12.dist-info → pyfemtet-0.4.2.dist-info}/LICENSE +0 -0
pyfemtet/opt/monitor.py DELETED
@@ -1,474 +0,0 @@
1
- import webbrowser
2
- import logging
3
- from time import sleep
4
- from threading import Thread
5
-
6
- import plotly.graph_objs as go
7
- import plotly.express as px
8
- from dash import Dash, html, dcc, ctx, Output, Input
9
- import dash_bootstrap_components as dbc
10
-
11
-
12
- def update_hypervolume_plot(history, df):
13
- # create figure
14
- fig = px.line(df, x="trial", y="hypervolume", markers=True)
15
-
16
- return fig
17
-
18
-
19
- def update_scatter_matrix(history, data):
20
- # data setting
21
- obj_names = history.obj_names
22
-
23
- # create figure
24
- fig = go.Figure()
25
-
26
- # graphs setting dependent on n_objectives
27
- if len(obj_names) == 0:
28
- return fig
29
-
30
- elif len(obj_names) == 1:
31
- fig.add_trace(
32
- go.Scatter(
33
- x=data['trial'],
34
- y=data[obj_names[0]].values,
35
- mode='markers+lines',
36
- )
37
- )
38
- fig.update_layout(
39
- dict(
40
- title_text="単目的プロット",
41
- xaxis_title="解析実行回数(回)",
42
- yaxis_title=obj_names[0],
43
- )
44
- )
45
-
46
- elif len(obj_names) == 2:
47
- fig.add_trace(
48
- go.Scatter(
49
- x=data[obj_names[0]],
50
- y=data[obj_names[1]],
51
- mode='markers',
52
- )
53
- )
54
- fig.update_layout(
55
- dict(
56
- title_text="多目的ペアプロット",
57
- xaxis_title=obj_names[0],
58
- yaxis_title=obj_names[1],
59
- )
60
- )
61
-
62
- elif len(obj_names) >= 3:
63
- fig.add_trace(
64
- go.Splom(
65
- dimensions=[dict(label=c, values=data[c]) for c in obj_names],
66
- diagonal_visible=False,
67
- showupperhalf=False,
68
- )
69
- )
70
- fig.update_layout(
71
- dict(
72
- title_text="多目的ペアプロット",
73
- )
74
- )
75
-
76
- return fig
77
-
78
-
79
- def create_home_layout():
80
- # components の設定
81
- # https://dash-bootstrap-components.opensource.faculty.ai/docs/components/accordion/
82
- dummy = html.Div('', id='dummy')
83
- interval = dcc.Interval(
84
- id='interval-component',
85
- interval=1*1000, # in milliseconds
86
- n_intervals=0,
87
- )
88
- header = html.H1("最適化の進行状況"),
89
- graphs = dbc.Card(
90
- [
91
- dbc.CardHeader(
92
- dbc.Tabs(
93
- [
94
- dbc.Tab(label="目的プロット", tab_id="tab-1"),
95
- dbc.Tab(label="Hypervolume", tab_id="tab-2"),
96
- ],
97
- id="card-tabs",
98
- active_tab="tab-1",
99
- )
100
- ),
101
- dbc.CardBody(html.P(id="card-content", className="card-text")),
102
- ]
103
- )
104
- status = dbc.Alert(
105
- children=[html.H4("optimization status here", className="alert-heading"),],
106
- color="primary",
107
- id='status-alert'
108
- )
109
- toggle_update_button = dbc.Button('グラフの自動更新の一時停止', id='toggle-update-button')
110
- interrupt_button = dbc.Button('最適化を中断', id='interrupt-button', color='danger')
111
- note_text = dcc.Markdown(f'''
112
- ---
113
- - このページでは、最適化の進捗状況を見ることができます。
114
- - このページを閉じても最適化は進行します。
115
- - この機能はブラウザによる状況確認機能ですが、インターネット通信は行いません。
116
- - 再びこのページを開くには、ブラウザのアドレスバーに __localhost:8080__ と入力してください。
117
- - ※ 特定のホスト名及びポートを指定するには、OptimizerBase.main() の実行前に
118
- OptimizerBase.set_monitor_host() を実行してください。
119
- ''')
120
-
121
- # layout の設定
122
- layout = dbc.Container([
123
- dbc.Row([dbc.Col(dummy), dbc.Col(interval)]),
124
- dbc.Row([dbc.Col(header)]),
125
- dbc.Row([dbc.Col(graphs)]),
126
- dbc.Row([dbc.Col(status)]),
127
- dbc.Row([dbc.Col(toggle_update_button), dbc.Col(interrupt_button)]),
128
- dbc.Row([dbc.Col(note_text)]),
129
- ], fluid=True)
130
-
131
- return layout
132
-
133
-
134
- def create_worker_monitor_layout(
135
- worker_addresses,
136
- worker_status_int_list
137
- ):
138
- from .base import OptimizationStatus
139
-
140
- interval = dcc.Interval(
141
- id='worker-status-update-interval',
142
- interval=1*1000, # in milliseconds
143
- n_intervals=0,
144
- )
145
-
146
- rows = [interval]
147
- for i, (worker_address, status_int) in enumerate(zip(worker_addresses, worker_status_int_list)):
148
- status_msg = OptimizationStatus.const_to_str(status_int)
149
- a = dbc.Alert(
150
- children=[
151
- f'({worker_address}) ',
152
- html.P(
153
- status_msg,
154
- id=f'worker-status-msg-{i}'
155
- ),
156
- ],
157
- id=f'worker-status-color-{i}',
158
- color="primary",
159
- )
160
- rows.append(dbc.Row([dbc.Col(a)]))
161
-
162
- layout = dbc.Container(
163
- rows,
164
- fluid=True
165
- )
166
-
167
- return layout
168
-
169
-
170
-
171
- class Monitor(object):
172
-
173
- def __init__(self, history, status, worker_addresses, worker_status_list):
174
-
175
- from .base import OptimizationStatus
176
-
177
- # 引数の処理
178
- self.history = history
179
- self.status = status
180
-
181
- # メインスレッドで更新してもらうメンバー
182
- self.current_status_int = self.status.get()
183
- self.current_status = self.status.get_text()
184
- self.current_worker_status_list = [s.get() for s in worker_status_list]
185
- self.df = self.history.actor_data.copy()
186
-
187
- # ログファイルの保存場所
188
- log_path = self.history.path.replace('.csv', '.uilog')
189
- l = logging.getLogger()
190
- l.addHandler(logging.FileHandler(log_path))
191
-
192
- # app の立上げ
193
- self.app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
194
-
195
- # ページの components と layout の設定
196
- self.home = create_home_layout()
197
- self.worker_monitor = create_worker_monitor_layout(worker_addresses, self.current_worker_status_list)
198
-
199
- # setup sidebar
200
- # https://dash-bootstrap-components.opensource.faculty.ai/examples/simple-sidebar/
201
-
202
- # the style arguments for the sidebar. We use position:fixed and a fixed width
203
- SIDEBAR_STYLE = {
204
- "position": "fixed",
205
- "top": 0,
206
- "left": 0,
207
- "bottom": 0,
208
- "width": "16rem",
209
- "padding": "2rem 1rem",
210
- "background-color": "#f8f9fa",
211
- }
212
-
213
- # the styles for the main content position it to the right of the sidebar and
214
- # add some padding.
215
- CONTENT_STYLE = {
216
- "margin-left": "18rem",
217
- "margin-right": "2rem",
218
- "padding": "2rem 1rem",
219
- }
220
- sidebar = html.Div(
221
- [
222
- html.H2("PyFemtet Monitor", className="display-4"),
223
- html.Hr(),
224
- html.P(
225
- "最適化の進捗を可視化します.", className="lead"
226
- ),
227
- dbc.Nav(
228
- [
229
- dbc.NavLink("Home", href="/", active="exact"),
230
- dbc.NavLink("Workers", href="/page-1", active="exact"),
231
- ],
232
- vertical=True,
233
- pills=True,
234
- ),
235
- ],
236
- style=SIDEBAR_STYLE,
237
- )
238
- content = html.Div(id="page-content", style=CONTENT_STYLE)
239
- self.app.layout = html.Div([dcc.Location(id="url"), sidebar, content])
240
-
241
- # sidebar によるページ遷移のための callback
242
- @self.app.callback(Output("page-content", "children"), [Input("url", "pathname")])
243
- def render_page_content(pathname):
244
- if pathname == "/": # p0
245
- return self.home
246
- elif pathname == "/page-1":
247
- return self.worker_monitor
248
- # elif pathname == "/page-2":
249
- # return html.P("Oh cool, this is page 2!")
250
- # If the user tries to reach a different page, return a 404 message
251
- return html.Div(
252
- [
253
- html.H1("404: Not found", className="text-danger"),
254
- html.Hr(),
255
- html.P(f"The pathname {pathname} was not recognised..."),
256
- ],
257
- className="p-3 bg-light rounded-3",
258
- )
259
-
260
- # 1. 一定時間ごとに ==> 自動更新が有効なら figure を更新する
261
- # 2. 中断ボタンを押したら ==> interrupt をセットする
262
- # 3. メイン処理が終了していたら ==> 更新を無効にする and 中断を無効にする
263
- # 4. toggle_button が押されたら ==> 更新を有効にする or 更新を無効にする
264
- # 5. タブを押したら ==> グラフの種類を切り替える
265
- # 6. state に応じて を切り替える
266
- @self.app.callback(
267
- [
268
- Output('interval-component', 'max_intervals'), # 3 4
269
- Output('interrupt-button', 'disabled'), # 3
270
- Output('toggle-update-button', 'disabled'), # 3 4
271
- Output('toggle-update-button', 'children'), # 3 4
272
- Output('card-content', 'children'), # 1 5
273
- Output('status-alert', 'children'), # 6
274
- Output('status-alert', 'color'), # 6
275
- ],
276
- [
277
- Input('interval-component', 'n_intervals'), # 1 3
278
- Input('toggle-update-button', 'n_clicks'), # 4
279
- Input('interrupt-button', 'n_clicks'), # 2
280
- Input("card-tabs", "active_tab"), # 5
281
- ]
282
- )
283
- def control(
284
- _, # n_intervals
285
- toggle_n_clicks,
286
- interrupt_n_clicks,
287
- active_tab_id,
288
- ):
289
- # 引数の処理
290
- toggle_n_clicks = 0 if toggle_n_clicks is None else toggle_n_clicks
291
- interrupt_n_clicks = 0 if interrupt_n_clicks is None else interrupt_n_clicks
292
-
293
- # 下記を基本に戻り値を上書きしていく(優先のものほど下に来る)
294
- max_intervals = -1 # enable
295
- button_disable = False
296
- toggle_text = 'グラフの自動更新を一時停止する'
297
- graph = None
298
- status_color = 'primary'
299
-
300
- # toggle_button が奇数なら interval を disable にする
301
- if toggle_n_clicks % 2 == 1:
302
- max_intervals = 0 # disable
303
- button_disable = False
304
- toggle_text = 'グラフの自動更新を再開する'
305
-
306
- # 終了なら interval とボタンを disable にする
307
- if self.current_status_int == OptimizationStatus.TERMINATED:
308
- max_intervals = 0 # disable
309
- button_disable = True
310
- toggle_text = 'グラフの更新は行われません'
311
-
312
- # 中断ボタンが押されたなら中断状態にする
313
- button_id = ctx.triggered_id if not None else 'No clicks yet'
314
- if button_id == 'interrupt-button':
315
- self.current_status_int = OptimizationStatus.INTERRUPTING
316
- self.current_status = OptimizationStatus.const_to_str(OptimizationStatus.INTERRUPTING)
317
-
318
- # グラフを更新する
319
- if active_tab_id is not None:
320
- if active_tab_id == "tab-1":
321
- graph = dcc.Graph(figure=update_scatter_matrix(self.history, self.df))
322
- elif active_tab_id == "tab-2":
323
- graph = dcc.Graph(figure=update_hypervolume_plot(self.history, self.df))
324
-
325
- # status を更新する
326
- status_children = [
327
- html.H4(
328
- 'optimization status: ' + self.current_status,
329
- className="alert-heading"
330
- ),
331
- ]
332
- if self.current_status_int == OptimizationStatus.INTERRUPTING:
333
- status_color = 'warning'
334
- if self.current_status_int == OptimizationStatus.TERMINATED:
335
- status_color = 'dark'
336
- if self.current_status_int == OptimizationStatus.TERMINATE_ALL:
337
- status_color = 'dark'
338
-
339
- return max_intervals, button_disable, button_disable, toggle_text, graph, status_children, status_color
340
-
341
- # worker_monitor のための callback
342
- @self.app.callback(
343
- [Output(f'worker-status-msg-{i}', 'children') for i in range(len(worker_addresses))],
344
- [Output(f'worker-status-color-{i}', 'color') for i in range(len(worker_addresses))],
345
- [Input('worker-status-update-interval', 'n_intervals'),]
346
- )
347
- def update_worker_state(_):
348
- msgs = [OptimizationStatus.const_to_str(i) for i in self.current_worker_status_list]
349
-
350
- colors = []
351
- for status_int in self.current_worker_status_list:
352
- if status_int == OptimizationStatus.INTERRUPTING:
353
- colors.append('warning')
354
- elif status_int == OptimizationStatus.TERMINATED:
355
- colors.append('dark')
356
- elif status_int == OptimizationStatus.TERMINATE_ALL:
357
- colors.append('dark')
358
- else:
359
- colors.append('primary')
360
-
361
- ret = msgs
362
- ret.extend(colors)
363
-
364
- return tuple(ret)
365
-
366
- def _run_server_forever(self, app, host, port):
367
- app.run(debug=False, host=host, port=port)
368
-
369
- def start_server(
370
- self,
371
- worker_addresses,
372
- worker_status_list, # [actor]
373
- host='localhost',
374
- port=8080,
375
- ):
376
-
377
- from .base import OptimizationStatus
378
-
379
- # 引数の処理
380
- if host is None:
381
- host = 'localhost'
382
- if port is None:
383
- port = 8080
384
-
385
- # ブラウザを起動
386
- if host == '0.0.0.0':
387
- webbrowser.open(f'http://localhost:{str(port)}')
388
- else:
389
- webbrowser.open(f'http://{host}:{str(port)}')
390
-
391
- # dash app server を daemon thread で起動
392
- server_thread = Thread(
393
- target=self._run_server_forever,
394
- args=(self.app, host, port,),
395
- daemon=True,
396
- )
397
- server_thread.start()
398
-
399
- # dash app (=flask server) の callback で dask の actor にアクセスすると
400
- # おかしくなることがあるので、ここで必要な情報のみやり取りする
401
- while True:
402
- # running 以前に monitor が current status を interrupting にしていれば actor に反映
403
- if (
404
- (self.status.get() <= OptimizationStatus.RUNNING) # メインプロセスが RUNNING 以前である
405
- and
406
- (self.current_status_int == OptimizationStatus.INTERRUPTING) # monitor の status が INTERRUPT である
407
- ):
408
- self.status.set(OptimizationStatus.INTERRUPTING)
409
-
410
- # current status と df を actor から monitor に反映する
411
- self.current_status_int = self.status.get()
412
- self.current_status = self.status.get_text()
413
- self.df = self.history.actor_data.copy()
414
- self.current_worker_status_list = [s.get() for s in worker_status_list]
415
-
416
- # terminate_all 指令があれば monitor server をホストするプロセスごと終了する
417
- if self.status.get() == OptimizationStatus.TERMINATE_ALL:
418
- return 0 # take server down with me
419
-
420
- # interval
421
- sleep(1)
422
-
423
- #
424
- # if __name__ == '__main__':
425
- # import datetime
426
- # import numpy as np
427
- # import pandas as pd
428
- #
429
- #
430
- # class IPV:
431
- # def __init__(self):
432
- # self.state = 'running'
433
- #
434
- # def get_state(self):
435
- # return self.state
436
- #
437
- # def set_state(self, state):
438
- # self.state = state
439
- #
440
- #
441
- # class History:
442
- # def __init__(self):
443
- # self.obj_names = 'A B C D E'.split()
444
- # self.path = 'tmp.csv'
445
- # self.data = None
446
- # t = Thread(target=self.update)
447
- # t.start()
448
- #
449
- # def update(self):
450
- #
451
- # d = dict(
452
- # trial=range(5),
453
- # hypervolume=np.random.rand(5),
454
- # time=[datetime.datetime(year=2000, month=1, day=1, second=s) for s in range(5)]
455
- # )
456
- # for obj_name in self.obj_names:
457
- # d[obj_name] = np.random.rand(5)
458
- #
459
- # while True:
460
- # self.data = pd.DataFrame(d)
461
- # sleep(1)
462
- #
463
- #
464
- # class FEMOPT:
465
- # def __init__(self, history, ipv):
466
- # self.history = history
467
- # self.ipv = ipv
468
- #
469
- #
470
- # _ipv = IPV()
471
- # _history = History()
472
- # _femopt = FEMOPT(_history, _ipv)
473
- # monitor = Monitor(_femopt)
474
- # monitor.start_server()
@@ -1,26 +0,0 @@
1
- pyfemtet/FemtetPJTSample/NX_ex01/NX_ex01.femprj,sha256=JfzRl_C72doQFJO0hJq8BTX6TSFB_Skh2C4l-kiWoXY,170268
2
- pyfemtet/FemtetPJTSample/NX_ex01/NX_ex01.prt,sha256=3okHLeMdslrRA_wkhppZtxIe-2-ZPMfNqWCdQwUV31o,226626
3
- pyfemtet/FemtetPJTSample/NX_ex01/NX_ex01.py,sha256=7irutFX1askkO3ck5UxcOEWq4vfZlIp9hzxBiChsuig,3060
4
- pyfemtet/FemtetPJTSample/Sldworks_ex01/Sldworks_ex01.SLDPRT,sha256=U0Yh559Fygd5sp013NwwhZ5vRA8D_E6kmiUXDP5isJQ,83094
5
- pyfemtet/FemtetPJTSample/Sldworks_ex01/Sldworks_ex01.femprj,sha256=omF3QvS8gzi_fSr2yobbVVaspR2YzDiUcMYvei-8ZmQ,155307
6
- pyfemtet/FemtetPJTSample/Sldworks_ex01/Sldworks_ex01.py,sha256=Zsv4ycIxESGsRjanfvcXCsz9VEMmGSBWHGNEe-3gN70,3897
7
- pyfemtet/FemtetPJTSample/gau_ex08_parametric.femprj,sha256=EguPWZHcwZMMX8cX1rZhLc2Pr__P4PR8RF4_n4uxDaI,268761
8
- pyfemtet/FemtetPJTSample/gau_ex08_parametric.py,sha256=z8IpzSexzP649GZCNTEiuxBkM-60Le8u7kBz9ixNfiY,2009
9
- pyfemtet/FemtetPJTSample/her_ex40_parametric.femprj,sha256=M0SP02stL2_Kb1e3tH_1YyFIg0JuYLIYTfPyvGw9jVY,129599
10
- pyfemtet/FemtetPJTSample/her_ex40_parametric.py,sha256=vibV5dPWxGAPMLJzH0cUZj24xdn0cO3Onl2uWT0H4XA,5168
11
- pyfemtet/FemtetPJTSample/wat_ex14_parallel_parametric.py,sha256=mebtu7u_01MslOj-H4IbG8Zt_KZ2FttXKrBLVTI2YME,2397
12
- pyfemtet/FemtetPJTSample/wat_ex14_parametric.femprj,sha256=TYXKt5ZFbsXsEsnmLGX5m-lSBZe8PHT7EFj0IqkrCwM,175152
13
- pyfemtet/FemtetPJTSample/wat_ex14_parametric.py,sha256=5QSim_Iuc5uRHllMeLf0YgCUVQpiuwXByA4IV8EINpw,2285
14
- pyfemtet/__init__.py,sha256=4iZzCiPqmeFKHtJkBXOmC-Pq2LNrWPJ_kikeL675y7k,22
15
- pyfemtet/core.py,sha256=PLVvdS3aTUcRKYkGOmkKWRg1r0zkOqPgBhlKRJZ988I,881
16
- pyfemtet/dispatch_extensions.py,sha256=kvAiurJHqHaMywpttiYeauqm3DUnitXjqNLEK4mM0O4,16053
17
- pyfemtet/logger.py,sha256=JYD0FvzijMS2NvZN7VT7vZA5hqtHEkvS93AHlIMDePw,2507
18
- pyfemtet/opt/_FemtetWithNX/update_model.py,sha256=t0AB7mKY7rmrI_9stP1-5qhzmugEQ19DnZ4CCrCdTSw,2856
19
- pyfemtet/opt/__init__.py,sha256=7kWMkod2J5hiua7AHt8bJbuiGqL0TEdiNtCIwbr2K1c,142
20
- pyfemtet/opt/base.py,sha256=ZN1HIkg71fcb27u3FZgmOP98DGh4_qCeUyWTUSvxGWM,55312
21
- pyfemtet/opt/interface.py,sha256=1SIo-bfqndU43JIxgYAmTNNWzIhhLN-u01FfAkuyBGk,34128
22
- pyfemtet/opt/monitor.py,sha256=LzspwrrCfrwLcXZ3dT9zziQxD39Q8BJt7sGY5Hm2Hro,16876
23
- pyfemtet-0.3.12.dist-info/LICENSE,sha256=sVQBhyoglGJUu65-BP3iR6ujORI6YgEU2Qm-V4fGlOA,1485
24
- pyfemtet-0.3.12.dist-info/METADATA,sha256=hlGcB8vx9DKpcnNS2NZhkv0dy9zuyc4BJuqMFxZgVYA,1724
25
- pyfemtet-0.3.12.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
26
- pyfemtet-0.3.12.dist-info/RECORD,,