osis-python 0.5.3__tar.gz → 0.5.5__tar.gz

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.
Files changed (117) hide show
  1. {osis_python-0.5.3/src/osis_python.egg-info → osis_python-0.5.5}/PKG-INFO +1 -1
  2. {osis_python-0.5.3 → osis_python-0.5.5/src/osis_python.egg-info}/PKG-INFO +1 -1
  3. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/__init__.py +1 -1
  4. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/boundary/interface.py +3 -3
  5. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/boundary/manager.py +1 -1
  6. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/core/apdl_sync.py +13 -22
  7. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/core/engine.py +28 -10
  8. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/core/sync_apdl_cli.py +3 -3
  9. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/element/manager.py +9 -0
  10. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/project/interface.py +1 -1
  11. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/project/manager.py +1 -1
  12. osis_python-0.5.5/src/pyosis/transfer/out_to_python.py +415 -0
  13. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/transfer/parser.py +53 -53
  14. osis_python-0.5.3/src/pyosis/transfer/out_to_python.py +0 -246
  15. {osis_python-0.5.3 → osis_python-0.5.5}/LICENSE +0 -0
  16. {osis_python-0.5.3 → osis_python-0.5.5}/README.md +0 -0
  17. {osis_python-0.5.3 → osis_python-0.5.5}/pyproject.toml +0 -0
  18. {osis_python-0.5.3 → osis_python-0.5.5}/setup.cfg +0 -0
  19. {osis_python-0.5.3 → osis_python-0.5.5}/src/osis_python.egg-info/SOURCES.txt +0 -0
  20. {osis_python-0.5.3 → osis_python-0.5.5}/src/osis_python.egg-info/dependency_links.txt +0 -0
  21. {osis_python-0.5.3 → osis_python-0.5.5}/src/osis_python.egg-info/requires.txt +0 -0
  22. {osis_python-0.5.3 → osis_python-0.5.5}/src/osis_python.egg-info/top_level.txt +0 -0
  23. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/boundary/__init__.py +0 -0
  24. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/control/__init__.py +0 -0
  25. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/control/control.py +0 -0
  26. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/control/io.py +0 -0
  27. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/control/manager.py +0 -0
  28. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/control/nonlinear.py +0 -0
  29. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/core/__init__.py +0 -0
  30. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/core/all_func.py +0 -0
  31. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/core/basic_manager.py +0 -0
  32. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/core/build.py +0 -0
  33. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/core/client.py +0 -0
  34. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/core/command.py +0 -0
  35. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/core/exceptions.py +0 -0
  36. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/core/references.py +0 -0
  37. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/display/__init__.py +0 -0
  38. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/display/interface.py +0 -0
  39. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/display/manager.py +0 -0
  40. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/dynamic/__init__.py +0 -0
  41. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/dynamic/load_to_mass.py +0 -0
  42. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/dynamic/manager.py +0 -0
  43. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/dynamic/modal.py +0 -0
  44. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/dynamic/seismic.py +0 -0
  45. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/element/__init__.py +0 -0
  46. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/element/interface.py +0 -0
  47. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/general/__init__.py +0 -0
  48. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/general/interface.py +0 -0
  49. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/geometry/__init__.py +0 -0
  50. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/geometry/interface.py +0 -0
  51. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/geometry/manager.py +0 -0
  52. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/live/__init__.py +0 -0
  53. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/live/analysis.py +0 -0
  54. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/live/grade.py +0 -0
  55. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/live/lane.py +0 -0
  56. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/live/manager.py +0 -0
  57. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/load/__init__.py +0 -0
  58. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/load/loadcase.py +0 -0
  59. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/load/manager.py +0 -0
  60. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/load/static.py +0 -0
  61. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/load/tendon.py +0 -0
  62. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/material/__init__.py +0 -0
  63. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/material/interface.py +0 -0
  64. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/material/manager.py +0 -0
  65. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/node/__init__.py +0 -0
  66. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/node/interface.py +0 -0
  67. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/node/manager.py +0 -0
  68. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/post/__init__.py +0 -0
  69. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/post/combine.py +0 -0
  70. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/post/design_check.py +0 -0
  71. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/post/display.py +0 -0
  72. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/post/manager.py +0 -0
  73. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/project/__init__.py +0 -0
  74. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/property/__init__.py +0 -0
  75. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/property/component_thickness.py +0 -0
  76. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/property/coordinate.py +0 -0
  77. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/property/creep_shrink.py +0 -0
  78. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/property/damping.py +0 -0
  79. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/property/manager.py +0 -0
  80. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/property/pu_curve.py +0 -0
  81. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/quick_building/__init__.py +0 -0
  82. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/quick_building/interface.py +0 -0
  83. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/result/__init__.py +0 -0
  84. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/result/check.py +0 -0
  85. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/result/env.py +0 -0
  86. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/result/loadcase.py +0 -0
  87. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/result/manager.py +0 -0
  88. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/result/result.py +0 -0
  89. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/section/__init__.py +0 -0
  90. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/section/common.py +0 -0
  91. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/section/composite.py +0 -0
  92. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/section/manager.py +0 -0
  93. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/section/numeric.py +0 -0
  94. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/section/param.py +0 -0
  95. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/section/rebar.py +0 -0
  96. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/section/rib.py +0 -0
  97. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/section/steel.py +0 -0
  98. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/settlement/__init__.py +0 -0
  99. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/settlement/interface.py +0 -0
  100. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/settlement/manager.py +0 -0
  101. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/stability/__init__.py +0 -0
  102. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/stability/buckling.py +0 -0
  103. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/stability/manager.py +0 -0
  104. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/stage/__init__.py +0 -0
  105. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/stage/define.py +0 -0
  106. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/stage/manager.py +0 -0
  107. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/stage/overall.py +0 -0
  108. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/thickness/__init__.py +0 -0
  109. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/thickness/interface.py +0 -0
  110. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/thickness/manager.py +0 -0
  111. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/transfer/__init__.py +0 -0
  112. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/transfer/generator.py +0 -0
  113. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/transfer/matrix.py +0 -0
  114. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/transfer/routes.py +0 -0
  115. {osis_python-0.5.3 → osis_python-0.5.5}/src/pyosis/transfer/split.py +0 -0
  116. {osis_python-0.5.3 → osis_python-0.5.5}/tests/test_delete_occupied.py +0 -0
  117. {osis_python-0.5.3 → osis_python-0.5.5}/tests/test_project_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: osis-python
3
- Version: 0.5.3
3
+ Version: 0.5.5
4
4
  Summary: A Python client library for Osis APDL
5
5
  Author-email: "CCCC Highway Consultant Co. Ltd." <support@osisbim.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: osis-python
3
- Version: 0.5.3
3
+ Version: 0.5.5
4
4
  Summary: A Python client library for Osis APDL
5
5
  Author-email: "CCCC Highway Consultant Co. Ltd." <support@osisbim.com>
6
6
  License-Expression: MIT
@@ -7,7 +7,7 @@ A library for extending OSIS functionality.
7
7
  A library that allows users to use CAE with great flexibility.
8
8
  """
9
9
 
10
- __version__ = "0.5.3"
10
+ __version__ = "0.5.5"
11
11
  __author__ = "CCCC Highway Consultant Co. Ltd." # 中交公路规划设计院 - 工程技术研究中心 - lzh - wjh - myf
12
12
 
13
13
  from .core.engine import OSISEngine
@@ -224,11 +224,11 @@ def osis_assign_boundary(nBd: int=1, eOP: Literal["a", "s", "r", "aa", "ra"]="a"
224
224
  pass
225
225
 
226
226
  @REGISTRY.register("BdGrp")
227
- def osis_boundary_group(strName: str, eOP: Literal["c", "a", "s", "r", "aa", "ra", "m", "d"], param: list=[]):
227
+ def osis_boundary_group(strName: str, eOP: Literal["c", "a", "s", "r", "aa", "ra", "m", "d"], *param: str | int):
228
228
  '''
229
229
  ## 添加或移除边界组
230
230
  pyosis.boundary.osis_boundary_group
231
-
231
+
232
232
  Args:
233
233
  strName (str): 边界组名
234
234
  eOP (str): 操作
@@ -240,7 +240,7 @@ def osis_boundary_group(strName: str, eOP: Literal["c", "a", "s", "r", "aa", "ra
240
240
  * ra = 移除全部
241
241
  * m = 修改组名
242
242
  * d = 删除
243
- param (list): 待操作的编号,支持的格式:*, *to*; *by*,仅用于替换。
243
+ param: 待操作的编号,支持的格式:*, *to*; *by*,仅用于替换。
244
244
  例子:[2,3,5,"8to10"] ["2by3","5by6","8by10"] 重合的编号自动忽略
245
245
 
246
246
  Returns:
@@ -396,7 +396,7 @@ class BoundaryGroup:
396
396
 
397
397
  def _execute(self, operation: Literal["c", "a", "s", "r", "aa", "ra", "m", "d"], *param: str | int) -> None:
398
398
  """执行边界组底层操作(内部使用)"""
399
- ok, err = osis_boundary_group(self.name, operation, list(param))
399
+ ok, err = osis_boundary_group(self.name, operation, *param)
400
400
  if not ok:
401
401
  raise RuntimeError(f"边界组操作 {self.name} ({operation}) 失败: {err}")
402
402
 
@@ -7,9 +7,9 @@
7
7
  4. 有变化 → parse → 写 prep → 存本次 hash
8
8
 
9
9
  用法:
10
- result = engine.sync_apdl()
11
- if result and result.changed:
12
- print(result.summary())
10
+ changed = engine.sync_apdl()
11
+ if changed:
12
+ print("命令流已变化,已写 prep")
13
13
  """
14
14
 
15
15
  from __future__ import annotations
@@ -21,7 +21,6 @@ from dataclasses import dataclass
21
21
  from pathlib import Path
22
22
  from typing import TYPE_CHECKING
23
23
 
24
- from ..transfer import parse_text
25
24
  from ..transfer.out_to_python import write_prep_outputs
26
25
 
27
26
  if TYPE_CHECKING:
@@ -39,19 +38,9 @@ _TIME_LINE_PATTERN = re.compile(
39
38
  )
40
39
 
41
40
 
42
- @dataclass
43
- class ApdlSyncResult:
44
- """APDL 同步结果。"""
45
-
46
- changed: bool = False
47
-
48
- def summary(self) -> str:
49
- return "命令流已变化,已写 prep" if self.changed else "命令流未变化"
50
-
51
-
52
41
  @dataclass
53
42
  class ApdlSessionStore:
54
- """记录最近一次同步的 .out 路径与 hash(供调试/展示,不由 ApdlSyncResult 重复携带)。"""
43
+ """记录最近一次同步的 .out 路径与 hash(供调试/展示)。"""
55
44
 
56
45
  last_path: str = ""
57
46
  last_hash: str = ""
@@ -162,11 +151,14 @@ def perform_apdl_sync(
162
151
  engine: "OSISEngine",
163
152
  path: str | None = None,
164
153
  *,
165
- force_export: bool = True,
166
- ) -> ApdlSyncResult | None:
154
+ force_export: bool = False,
155
+ ) -> bool | None:
167
156
  """执行 APDL 同步:export → 比 hash → 有变化则写 prep(不执行 prep)。
168
157
 
169
- 无项目路径时返回 None。
158
+ Returns:
159
+ True - 命令流已变化,已写 prep
160
+ False - 命令流未变化
161
+ None - 未获取到项目路径(无法同步)
170
162
  """
171
163
  if _get_project_dir(engine) is None:
172
164
  return None
@@ -186,12 +178,11 @@ def perform_apdl_sync(
186
178
  session.update_file(str(out_path), file_hash)
187
179
 
188
180
  if file_hash == last_hash:
189
- return ApdlSyncResult(changed=False)
181
+ return False
190
182
 
191
- parsed = parse_text(text)
192
- write_prep_outputs(parsed, prep_dir)
183
+ write_prep_outputs(out_path, prep_dir)
193
184
 
194
185
  # 未执行 prep,OSIS 未变
195
186
  _save_persisted_hash(state_path, out_path, file_hash)
196
187
 
197
- return ApdlSyncResult(changed=True)
188
+ return True
@@ -22,7 +22,7 @@ from __future__ import annotations
22
22
  import os
23
23
  from typing import Any
24
24
 
25
- from .apdl_sync import ApdlSyncResult, ApdlSessionStore, perform_apdl_sync
25
+ from .apdl_sync import ApdlSessionStore, perform_apdl_sync
26
26
  from ..material.manager import MaterialManager, material_manager
27
27
  from ..section.manager import SectionManager, section_manager
28
28
  from ..node.manager import NodeManager, node_manager
@@ -240,6 +240,15 @@ class OSISEngine:
240
240
  """显示管理器(边界 / 荷载 / 钢束显隐等)"""
241
241
  return self._display
242
242
 
243
+
244
+ def start(self) -> None:
245
+ """开启 OSIS 软件"""
246
+ ...
247
+
248
+ def close(self) -> None:
249
+ """关闭 OSIS 软件"""
250
+ ...
251
+
243
252
  # ──────────────────────────────────────────
244
253
  # 通用操作(直接暴露 general 和 core 的函数)
245
254
  # ──────────────────────────────────────────
@@ -335,10 +344,6 @@ class OSISEngine:
335
344
  """
336
345
  self._project.open(filepath)
337
346
 
338
- def close(self) -> None:
339
- """关闭 OSIS 软件"""
340
- self._project.close()
341
-
342
347
  # ──────────────────────────────────────────
343
348
  # 便捷方法(委托给 control manager)
344
349
  # ──────────────────────────────────────────
@@ -408,8 +413,21 @@ class OSISEngine:
408
413
  return f"OSISEngine(project_path={self.project.get_directory()})"
409
414
 
410
415
  def sync_apdl(
411
- self,
412
- path: str | None = None,
413
- ) -> ApdlSyncResult:
414
- """通过 APDL 导出从 OSIS 同步数据。OSIS 界面操作后调用。"""
415
- return perform_apdl_sync(self, path)
416
+ self,
417
+ path: str | None = None,
418
+ *,
419
+ force_export: bool = False,
420
+ ) -> bool | None:
421
+ """通过 APDL 导出从 OSIS 同步数据。OSIS 界面操作后调用。
422
+
423
+ Args:
424
+ path: 自定义 .out 文件路径,None 用项目下默认 _pyosis_sync.out。
425
+ force_export: False(默认)若 .out 已存在则直接复用,仅当 hash 与上次不一致才写 prep;
426
+ True 时总是重新执行 export_apdl。
427
+
428
+ Returns:
429
+ True - 命令流已变化,已写 prep
430
+ False - 命令流未变化
431
+ None - 未获取到项目路径
432
+ """
433
+ return perform_apdl_sync(self, path, force_export=force_export)
@@ -20,16 +20,16 @@ from .engine import OSISEngine
20
20
  def main() -> int:
21
21
  engine = OSISEngine()
22
22
  try:
23
- result = perform_apdl_sync(engine)
23
+ changed = perform_apdl_sync(engine)
24
24
  except Exception as exc:
25
25
  print(f"sync_apdl 失败: {exc}", file=sys.stderr)
26
26
  return 1
27
27
 
28
- if result is None:
28
+ if changed is None:
29
29
  print("跳过:未获取到项目路径")
30
30
  return 0
31
31
 
32
- print(result.summary())
32
+ print("命令流已变化,已写 prep" if changed else "命令流未变化")
33
33
  return 0
34
34
 
35
35
 
@@ -298,6 +298,15 @@ class ElementGroupManager:
298
298
  Args:
299
299
  name: 单元组名称
300
300
  op: 操作
301
+ * c = 创建
302
+ * a = 添加
303
+ * s = 替换
304
+ * r = 移除
305
+ * aa = 添加全部
306
+ * ra = 移除全部
307
+ * m = 修改组名
308
+ * d = 删除
309
+ *param (str | int): 待操作的编号,支持的格式:*,*to*;*by*,仅用于替换。例子:2,3,5,8to10,2by3,5by6,8by10 重合的编号自动忽略
301
310
 
302
311
  Returns:
303
312
  ElementGroup: 创建的单元组对象
@@ -67,7 +67,7 @@ def save_project_as(filePath) -> Tuple[bool, str]:
67
67
  @REGISTRY.register("/Close")
68
68
  def close_osis() -> Tuple[bool, str]:
69
69
  """
70
- 关闭 OSIS 软件
70
+ 关闭 OSIS 项目
71
71
 
72
72
  Returns:
73
73
  Tuple[bool, str]
@@ -86,7 +86,7 @@ class ProjectManager:
86
86
  raise RuntimeError(f"另存为失败: {err}")
87
87
 
88
88
  def close(self) -> None:
89
- """关闭 OSIS 软件
89
+ """关闭 OSIS 项目
90
90
 
91
91
  Raises:
92
92
  RuntimeError: 关闭失败时抛出异常
@@ -0,0 +1,415 @@
1
+ """transfer 同步链路的纯文本处理工具:把 .out 文本转换为 pyosis prep 模块。
2
+
3
+ 只负责文本解析与 Python 代码生成:
4
+ - 读 .out 源文件
5
+ - 用 parser 解析出命令流,并按 //--- MODULE_NAME --- 标记分桶
6
+ - 第一个模块标记之前的"前言"命令(典型为 clear/clc)写到 main.py
7
+ - 每个模块的命令写到 _1_control.py … _10_stage.py
8
+
9
+ 不负责:
10
+ - .out 文件的获取(export_apdl) —— 由调用方管理
11
+ - prep 模块的执行(import + builder 调用) —— 由 main.py 调度
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import inspect
17
+ import re
18
+ import sys
19
+ from collections import defaultdict
20
+ from pathlib import Path
21
+ from typing import Iterable
22
+
23
+ from pyosis.transfer.generator import generate_lines
24
+ from pyosis.transfer.parser import (
25
+ BLANK_RE,
26
+ COMMENT_LINE_RE,
27
+ ParsedCommand,
28
+ _parse_physical_line,
29
+ iter_physical_lines,
30
+ strip_inline_comment,
31
+ )
32
+
33
+ # 匹配链式调用中的 .get(...) 部分(如 engine.section.get("k").set_offset(...))
34
+ _CHAIN_GET_RE = re.compile(r"""\.get\((?:"[^"]*"|'[^']*'|[^()]*)\)""")
35
+
36
+ # .out 中模块标记行的正则: //----- CONTROL ----- 之类
37
+ MODULE_PATTERN = re.compile(r"^\s*//-+\s*(\w+)\s*-*")
38
+ # 已知的模块名(用于过滤 .out 中的非模块横线注释,例如 //--- VERSION: x.xx ---//)
39
+ KNOWN_MODULES = frozenset({
40
+ "CONTROL",
41
+ "PROPERTY",
42
+ "MATERIAL",
43
+ "SECTION",
44
+ "NODE",
45
+ "ELEMENT",
46
+ "BOUNDARY",
47
+ "LOADCASE",
48
+ "ANALYSIS",
49
+ "STAGE",
50
+ })
51
+
52
+ # 模块文件名
53
+ MODULE_FILES = {
54
+ "CONTROL": "_1_control.py",
55
+ "PROPERTY": "_2_property.py",
56
+ "MATERIAL": "_3_material.py",
57
+ "SECTION": "_4_section.py",
58
+ "NODE": "_5_node.py",
59
+ "ELEMENT": "_6_element.py",
60
+ "BOUNDARY": "_7_boundary.py",
61
+ "LOADCASE": "_8_loadcase.py",
62
+ "ANALYSIS": "_9_analysis.py",
63
+ "STAGE": "_10_stage.py",
64
+ }
65
+
66
+ # 模块生成函数名
67
+ MODULE_BUILDERS = {
68
+ "CONTROL": "setup_control",
69
+ "PROPERTY": "build_property",
70
+ "MATERIAL": "build_materials",
71
+ "SECTION": "build_sections",
72
+ "NODE": "build_nodes",
73
+ "ELEMENT": "build_elements",
74
+ "BOUNDARY": "build_boundaries",
75
+ "LOADCASE": "build_loadcases",
76
+ "ANALYSIS": "build_analysis",
77
+ "STAGE": "build_stages",
78
+ }
79
+
80
+ MODULE_ORDER = tuple(MODULE_FILES.keys())
81
+
82
+ # 模块职责一句话(给 AI 看的提示)
83
+ MODULE_PURPOSE = {
84
+ "CONTROL": "全局控制参数(重力、非线性、收缩徐变开关等)",
85
+ "PROPERTY": "几何属性(坐标系、收缩徐变特性、钢束线型等)",
86
+ "MATERIAL": "材料定义(混凝土、钢筋、钢绞线)",
87
+ "SECTION": "截面定义(标准截面 + 加厚/变化截面)",
88
+ "NODE": "节点坐标",
89
+ "ELEMENT": "单元(梁/弹簧)创建 + 分组",
90
+ "BOUNDARY": "边界条件(支座、约束自由度)",
91
+ "LOADCASE": "荷载工况(自重、二期、预应力、温度、沉降)",
92
+ "ANALYSIS": "分析设置(活载等级、车道)",
93
+ "STAGE": "施工阶段(激活/钝化、体系转换)",
94
+ }
95
+
96
+
97
+ # 按模块分桶
98
+ def _split_by_module(text: str) -> tuple[list[ParsedCommand], dict[str, list[ParsedCommand]]]:
99
+ """走一遍 .out 文本,把命令按 //--- MODULE_NAME --- 标记分桶。
100
+
101
+ Returns:
102
+ (preamble, {MODULE: [cmd, ...]})
103
+ - preamble: 第一个模块标记之前的命令(典型为 clear/clc)
104
+ - by_module: 每个模块的命令列表,按 .out 出现顺序
105
+ """
106
+ preamble: list[ParsedCommand] = []
107
+ by_module: dict[str, list[ParsedCommand]] = defaultdict(list)
108
+ current: str | None = None
109
+
110
+ for line in iter_physical_lines(text):
111
+ m = MODULE_PATTERN.match(line)
112
+ if m:
113
+ name = m.group(1).upper()
114
+ current = name if name in KNOWN_MODULES else None
115
+ continue
116
+ if COMMENT_LINE_RE.match(line) or BLANK_RE.match(line):
117
+ continue
118
+ line = strip_inline_comment(line)
119
+ if BLANK_RE.match(line):
120
+ continue
121
+ cmds = _parse_physical_line(line)
122
+ if current is None:
123
+ preamble.extend(cmds)
124
+ else:
125
+ by_module[current].extend(cmds)
126
+
127
+ return preamble, dict(by_module)
128
+
129
+
130
+ # 读取 .out 文件内容(容错编码)
131
+ def _read_out_text(path: Path) -> str:
132
+ for encoding in ("gbk", "utf-8", "gb2312", "gb18030", "latin-1"):
133
+ try:
134
+ return path.read_text(encoding=encoding)
135
+ except UnicodeDecodeError:
136
+ continue
137
+ raise RuntimeError(f"无法解码 .out 文件: {path}")
138
+
139
+
140
+ # ──────────────────────────────────────────────────────────────────────
141
+ # 方法 docstring 注释:在每个被调用方法的第一次出现处,插入其 docstring 第一行
142
+ # ──────────────────────────────────────────────────────────────────────
143
+
144
+ _ENGINE_INSTANCE: object | None = None
145
+
146
+
147
+ def _get_engine() -> object:
148
+ """懒加载一个 OSISEngine 实例,只读属性,不会真正连 OSIS。"""
149
+ global _ENGINE_INSTANCE
150
+ if _ENGINE_INSTANCE is None:
151
+ from pyosis.core.engine import OSISEngine
152
+ _ENGINE_INSTANCE = OSISEngine()
153
+ return _ENGINE_INSTANCE
154
+
155
+
156
+ def _strip_outer_args(line: str) -> str:
157
+ """剥离最外层调用的实参列表,返回接收者表达式。
158
+
159
+ 取的是最后一个顶层 `(`,这样链式调用也能保留中间的 .get(...)。
160
+ engine.control.set_gravity_acceleration(9.8) → engine.control.set_gravity_acceleration
161
+ engine.section.get(1).set_offset("Middle", 0) → engine.section.get(1).set_offset
162
+ engine.matrix("M", [[1]]) → engine.matrix
163
+ """
164
+ depth = 0
165
+ last_paren = -1
166
+ in_str = False
167
+ sc: str | None = None
168
+ for i, ch in enumerate(line):
169
+ if in_str:
170
+ if ch == sc and (i == 0 or line[i - 1] != "\\"):
171
+ in_str = False
172
+ continue
173
+ if ch in ('"', "'"):
174
+ in_str = True
175
+ sc = ch
176
+ continue
177
+ if ch == "(":
178
+ if depth == 0:
179
+ last_paren = i
180
+ depth += 1
181
+ elif ch == ")":
182
+ depth -= 1
183
+ return line[:last_paren] if last_paren >= 0 else line
184
+
185
+
186
+ def _doc_first_line(doc: str | None) -> str:
187
+ if not doc:
188
+ return ""
189
+ return doc.strip().split("\n", 1)[0].strip()
190
+
191
+
192
+ def _resolve_chain_target(head: str, engine: object) -> object | None:
193
+ """从 'engine.X.method'(已折叠 .get)解析出真正的可调用对象。
194
+
195
+ 1) 直接 getattr 链:engine.section.create → SectionManager.create;engine.matrix → OSISEngine.matrix
196
+ 2) 链式调用:head 折叠后是 engine.section.set_offset(或嵌套的 engine.live.case.include),
197
+ 中间某一层管理器没有这个属性。从该层往前找带 .get 的管理器,用其返回类型注解
198
+ (Section / LiveCase / TendonShape 等)找到方法本体。
199
+ """
200
+ parts = head.split(".")
201
+ if len(parts) < 2 or parts[0] != "engine":
202
+ return None
203
+
204
+ # 1) 直接 getattr 链
205
+ try:
206
+ obj = engine
207
+ for p in parts[1:]:
208
+ obj = getattr(obj, p)
209
+ if callable(obj):
210
+ return obj
211
+ except AttributeError:
212
+ pass
213
+
214
+ if len(parts) < 3:
215
+ return None
216
+ method_name = parts[-1]
217
+
218
+ # 2) 从尾往头试每一层,看它是不是一个带 .get 的管理器
219
+ for cut in range(len(parts) - 1, 1, -1):
220
+ try:
221
+ mgr_obj = engine
222
+ for p in parts[1:cut]:
223
+ mgr_obj = getattr(mgr_obj, p)
224
+ except AttributeError:
225
+ continue
226
+ get_method = type(mgr_obj).__dict__.get("get")
227
+ if get_method is None:
228
+ continue
229
+ try:
230
+ sig = inspect.signature(get_method)
231
+ except (ValueError, TypeError):
232
+ continue
233
+ ann = sig.return_annotation
234
+ if ann is inspect.Parameter.empty:
235
+ continue
236
+ if isinstance(ann, str):
237
+ mod = sys.modules.get(get_method.__module__)
238
+ if mod is None:
239
+ continue
240
+ try:
241
+ ann = eval(ann, vars(mod))
242
+ except Exception:
243
+ continue
244
+ candidates: list[type] = []
245
+ if isinstance(ann, type):
246
+ candidates.append(ann)
247
+ else:
248
+ for arg in getattr(ann, "__args__", ()):
249
+ if isinstance(arg, type):
250
+ candidates.append(arg)
251
+ else:
252
+ for inner in getattr(arg, "__args__", ()):
253
+ if isinstance(inner, type):
254
+ candidates.append(inner)
255
+ for cand in candidates:
256
+ target = vars(cand).get(method_name)
257
+ if target is not None and callable(target):
258
+ return target
259
+ return None
260
+
261
+
262
+ def _method_doc_for(line: str) -> str:
263
+ """取这一行生成代码所调用方法的 docstring 第一行。非 engine 调用返空串。"""
264
+ s = line.strip()
265
+ if not s.startswith("engine."):
266
+ return ""
267
+ head = _CHAIN_GET_RE.sub("", _strip_outer_args(s))
268
+ # head 形如 'engine.section.set_offset' 或 'engine.control.set_gravity_acceleration'
269
+ target = _resolve_chain_target(head, _get_engine())
270
+ if target is None:
271
+ return ""
272
+ return _doc_first_line(getattr(target, "__doc__", None))
273
+
274
+
275
+ def _insert_method_doc_comments(lines: list[str]) -> list[str]:
276
+ """在每个被调用的独特方法的第一次出现处,插入一行 # <docstring 第一行>。"""
277
+ seen: set[str] = set()
278
+ out: list[str] = []
279
+ for line in lines:
280
+ s = line.strip()
281
+ if s.startswith("engine."):
282
+ # 用与解析时相同的 head 作为去重键,保证多次调用同一方法只注一次
283
+ head = _CHAIN_GET_RE.sub("", _strip_outer_args(s))
284
+ if head not in seen:
285
+ doc = _method_doc_for(line)
286
+ if doc:
287
+ out.append(f"# {doc}")
288
+ seen.add(head)
289
+ out.append(line)
290
+ return out
291
+
292
+
293
+ # 写入 prep 模块
294
+ def _write_prep_module(prep_dir: Path, module: str, lines: list[str]) -> Path:
295
+ """写入 prep 模块;lines 为空时写 pass stub。
296
+
297
+ 生成顺序:
298
+ 1. docstring(说明模块职责)
299
+ 2. imports
300
+ 3. builder 函数
301
+ 4. if __name__ == "__main__": 单跑入口
302
+ """
303
+ fname = MODULE_FILES[module]
304
+ builder = MODULE_BUILDERS[module]
305
+ empty = not lines
306
+ purpose = MODULE_PURPOSE.get(module, module)
307
+
308
+ if empty:
309
+ doc = f"OSIS 命令流 {module} 模块 — 当前 .out 中无该段,无需调用。"
310
+ fn_body = [" pass"]
311
+ else:
312
+ doc = f"OSIS 命令流 {module} 模块 — {purpose}"
313
+ annotated = _insert_method_doc_comments(lines)
314
+ fn_body = [f" {line}" for line in annotated]
315
+
316
+ body_lines = [
317
+ f'"""{doc}"""',
318
+ "",
319
+ "from __future__ import annotations",
320
+ "",
321
+ "from pyosis.core.engine import OSISEngine",
322
+ "",
323
+ f"def {builder}(engine: OSISEngine) -> None:",
324
+ *fn_body,
325
+ "",
326
+ 'if __name__ == "__main__":',
327
+ " from _0_engine import engine",
328
+ f" {builder}(engine)",
329
+ "",
330
+ ]
331
+ path = prep_dir / fname
332
+ path.write_text("\n".join(body_lines), encoding="utf-8")
333
+ return path
334
+
335
+
336
+ def _iter_module_call_lines() -> Iterable[str]:
337
+ """main.py 中依次调用各模块 builder 的语句(4 空格缩进)。"""
338
+ for mod in MODULE_ORDER:
339
+ yield f" {MODULE_BUILDERS[mod]}(engine)"
340
+
341
+
342
+ def _write_main_py(prep_dir: Path, preamble_lines: list[str]) -> Path:
343
+ """写入 main.py:先执行 preamble(典型为 clear/clc),再依次执行 _1.._10 builder。"""
344
+ import_lines = [f"from {Path(MODULE_FILES[mod]).stem} import {MODULE_BUILDERS[mod]}" for mod in MODULE_ORDER]
345
+
346
+ body_lines = [
347
+ '"""main.py — 入口脚本,按 _1.._10 顺序依次执行 prep 模块。',
348
+ "",
349
+ "可直接 `python main.py`,或被 import 后调 main()。",
350
+ "engine 用 _0_engine 模块级单例,跟同包内的 prep 模块保持一致。",
351
+ '"""',
352
+ "",
353
+ "from __future__ import annotations",
354
+ "",
355
+ "import sys",
356
+ "from pathlib import Path",
357
+ "",
358
+ "# 让 main.py 不管从哪个目录跑都能 import 同目录的 prep modules",
359
+ "sys.path.insert(0, str(Path(__file__).resolve().parent))",
360
+ "",
361
+ "from _0_engine import engine",
362
+ *import_lines,
363
+ "",
364
+ "def main() -> None:",
365
+ ]
366
+ for line in preamble_lines:
367
+ body_lines.append(f" {line}")
368
+ body_lines.append("")
369
+ body_lines.extend(_iter_module_call_lines())
370
+ body_lines.extend([
371
+ "",
372
+ 'if __name__ == "__main__":',
373
+ " main()",
374
+ "",
375
+ ])
376
+
377
+ path = prep_dir / "main.py"
378
+ path.write_text("\n".join(body_lines), encoding="utf-8")
379
+ return path
380
+
381
+
382
+ def write_prep_outputs(out_path: Path, prep_dir: Path) -> tuple[int, list[Path]]:
383
+ """读 .out 源文件,生成 _0_engine.py + _1~_10 prep 模块 + main.py,返回 (代码行数, 文件路径列表)。
384
+
385
+ 每个标准模块都会写入文件;.out 中无对应命令时生成 pass stub,避免遗留旧 prep。
386
+
387
+ Args:
388
+ out_path: 源 .out 文件路径(由 apdl_sync 管理获取,这里只读取)
389
+ prep_dir: prep 输出目录(必须已存在)
390
+ """
391
+ text = _read_out_text(out_path)
392
+ preamble, by_module = _split_by_module(text)
393
+
394
+ code_line_count = 0
395
+ prep_paths: list[Path] = []
396
+
397
+ for module in MODULE_ORDER:
398
+ cmds = by_module.get(module, [])
399
+ lines = generate_lines(cmds) if cmds else []
400
+ code_line_count += len(lines)
401
+ prep_paths.append(_write_prep_module(prep_dir, module, lines))
402
+
403
+ # 前言(典型 clear/clc)放到 main.py 顶部;无前言时为空列表
404
+ preamble_lines = generate_lines(preamble) if preamble else []
405
+
406
+ (prep_dir / "_0_engine.py").write_text(
407
+ '"""模块级单例 OSISEngine,供同包内其他 prep 模块 import 使用。\n\n'
408
+ "不要在主代码里直接 new OSISEngine();通过 main.py 调度。\n"
409
+ '"""\n'
410
+ "from pyosis.core.engine import OSISEngine\n\n"
411
+ "engine = OSISEngine()\n",
412
+ encoding="utf-8",
413
+ )
414
+ prep_paths.append(_write_main_py(prep_dir, preamble_lines))
415
+ return code_line_count, prep_paths