mphkit 0.2.0__tar.gz → 0.3.0__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 (32) hide show
  1. mphkit-0.3.0/PKG-INFO +325 -0
  2. mphkit-0.3.0/README.md +294 -0
  3. {mphkit-0.2.0 → mphkit-0.3.0}/pyproject.toml +6 -3
  4. {mphkit-0.2.0 → mphkit-0.3.0}/pyproject.toml.orig +9 -3
  5. mphkit-0.3.0/src/mphkit/__init__.py +176 -0
  6. mphkit-0.3.0/src/mphkit/_catalog.py +1158 -0
  7. mphkit-0.3.0/src/mphkit/_check.py +635 -0
  8. {mphkit-0.2.0 → mphkit-0.3.0}/src/mphkit/_comsol.py +289 -34
  9. mphkit-0.3.0/src/mphkit/_datasets.py +408 -0
  10. {mphkit-0.2.0 → mphkit-0.3.0}/src/mphkit/_expr.py +2 -5
  11. mphkit-0.3.0/src/mphkit/_hints.py +490 -0
  12. {mphkit-0.2.0 → mphkit-0.3.0}/src/mphkit/_image.py +174 -60
  13. mphkit-0.3.0/src/mphkit/_materials.py +437 -0
  14. {mphkit-0.2.0 → mphkit-0.3.0}/src/mphkit/_measure.py +25 -16
  15. mphkit-0.3.0/src/mphkit/_mesh.py +425 -0
  16. mphkit-0.3.0/src/mphkit/_plot.py +680 -0
  17. {mphkit-0.2.0 → mphkit-0.3.0}/src/mphkit/_props.py +10 -5
  18. mphkit-0.3.0/src/mphkit/_results.py +872 -0
  19. {mphkit-0.2.0 → mphkit-0.3.0}/src/mphkit/_sel.py +122 -31
  20. mphkit-0.3.0/src/mphkit/_solve.py +954 -0
  21. mphkit-0.3.0/src/mphkit/_sweep.py +1868 -0
  22. mphkit-0.3.0/src/mphkit/_winproc.py +332 -0
  23. mphkit-0.3.0/src/mphkit/errors.py +17 -0
  24. {mphkit-0.2.0 → mphkit-0.3.0}/src/mphkit/geometry.py +57 -13
  25. mphkit-0.2.0/PKG-INFO +0 -193
  26. mphkit-0.2.0/README.md +0 -162
  27. mphkit-0.2.0/src/mphkit/__init__.py +0 -117
  28. mphkit-0.2.0/src/mphkit/_hints.py +0 -274
  29. mphkit-0.2.0/src/mphkit/errors.py +0 -5
  30. {mphkit-0.2.0 → mphkit-0.3.0}/LICENSE +0 -0
  31. {mphkit-0.2.0 → mphkit-0.3.0}/src/mphkit/py.typed +0 -0
  32. {mphkit-0.2.0 → mphkit-0.3.0}/src/mphkit/sel.py +0 -0
mphkit-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,325 @@
1
+ Metadata-Version: 2.4
2
+ Name: mphkit
3
+ Version: 0.3.0
4
+ Summary: Helpers on top of MPh for COMSOL in Python: build geometry, select by location, insert library materials, check a model before solving, and read results as numbers or pictures.
5
+ Keywords: comsol,mph,multiphysics,geometry,selection,simulation,fem
6
+ Author: elgar328
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Operating System :: MacOS
12
+ Classifier: Operating System :: Microsoft :: Windows
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Scientific/Engineering
21
+ Classifier: Topic :: Scientific/Engineering :: Physics
22
+ Classifier: Typing :: Typed
23
+ Requires-Dist: mph>=1.4,<2
24
+ Requires-Dist: numpy
25
+ Requires-Python: >=3.10
26
+ Project-URL: Homepage, https://github.com/elgar328/mphkit
27
+ Project-URL: Repository, https://github.com/elgar328/mphkit
28
+ Project-URL: Changelog, https://github.com/elgar328/mphkit/blob/main/CHANGELOG.md
29
+ Project-URL: Issues, https://github.com/elgar328/mphkit/issues
30
+ Description-Content-Type: text/markdown
31
+
32
+ # mphkit
33
+
34
+ [![PyPI](https://img.shields.io/pypi/v/mphkit)](https://pypi.org/project/mphkit/)
35
+ [![Python](https://img.shields.io/python/required-version-toml?tomlFilePath=https://raw.githubusercontent.com/elgar328/mphkit/main/pyproject.toml)](https://pypi.org/project/mphkit/)
36
+ [![License](https://img.shields.io/pypi/l/mphkit)](https://github.com/elgar328/mphkit/blob/main/LICENSE)
37
+ [![COMSOL](https://img.shields.io/badge/COMSOL-6.4-blue)](https://www.comsol.com/)
38
+
39
+ Helpers on top of [MPh](https://github.com/MPh-py/MPh) for COMSOL in
40
+ Python: build geometry, select by location, insert library materials,
41
+ check a model before solving, and read results as numbers or pictures.
42
+ Selections made by location, not entity number, keep working when the
43
+ geometry changes.
44
+
45
+ > [!WARNING]
46
+ > **Early stage.** The API may change at any time, without deprecation
47
+ > warnings.
48
+
49
+ Not affiliated with COMSOL AB.
50
+
51
+ ## Example
52
+
53
+ ```python
54
+ import mph
55
+ import mphkit as mk
56
+
57
+ client = mph.start()
58
+ model = client.create('demo')
59
+ geom = mk.geometry(model, 3, length_unit='mm')
60
+
61
+ plate = mk.block(geom, (100, 100, 10), name='plate')
62
+ hole = mk.cylinder(geom, 5, 10, (50, 50, 0))
63
+ mk.difference(geom, plate, [hole])
64
+ model.build(geom)
65
+
66
+ bottom = mk.sel.box(geom, 'boundary', z=0) # faces on the plane z = 0
67
+
68
+ # plain MPh from here on
69
+ physics = (model/'physics').create('HeatTransfer', geom)
70
+ physics.create('TemperatureBoundary', 2).select(bottom) # 2: boundaries in 3D
71
+ model.save('demo.mph')
72
+ ```
73
+
74
+ Helpers take MPh `Node`s (`mk.geometry` the model, `mk.set` also Java
75
+ objects), and those that create something return one, so mphkit and MPh
76
+ mix freely. Physics, mesh and study stay plain MPh (or the COMSOL Java
77
+ API through `node.java`); mphkit looks up the COMSOL names they need.
78
+
79
+ `print(mphkit.__doc__)` shows the workflow as one script, the rules and
80
+ an index of every helper; `help(mk.<name>)` has the details of one,
81
+ `help(mk.sel)` those of the selections. Point an AI assistant to
82
+ `print(mphkit.__doc__)` first.
83
+
84
+ ## Requirements
85
+
86
+ - COMSOL Multiphysics with a license, installed where MPh can find it
87
+ (see the [MPh documentation](https://mph.readthedocs.io)).
88
+ - Python 3.10 or newer, MPh 1.4 or newer (below 2: mphkit uses some of
89
+ its internals).
90
+ - Importing CAD files (`mk.import_` with STEP, IGES, ...) needs a license
91
+ for CAD import (CAD Import Module, Design Module or a LiveLink).
92
+ Everything else needs COMSOL only.
93
+
94
+ mphkit is developed and tested with COMSOL 6.4 and MPh 1.4. Feature types,
95
+ property names and selection behavior can differ between COMSOL versions,
96
+ so other versions may need adjustments; reports are welcome. Checked on
97
+ macOS and Windows; it should run wherever MPh runs. The name lookups read
98
+ COMSOL's code-completion data in the installation (`data/completion`),
99
+ which COMSOL does not document; they were checked with COMSOL 6.4 on
100
+ macOS and Windows. A missing or changed catalogue raises an error rather
101
+ than giving wrong names.
102
+
103
+ ## Installation
104
+
105
+ ```
106
+ pip install mphkit
107
+ ```
108
+
109
+ or `uv add mphkit` in a uv project.
110
+
111
+ What changed between versions is listed in
112
+ [CHANGELOG.md](https://github.com/elgar328/mphkit/blob/main/CHANGELOG.md).
113
+
114
+ ## What it covers
115
+
116
+ Geometry, in 3D, 2D and in work planes:
117
+
118
+ ```python
119
+ mk.block(geom, (10, 10, 5)); mk.cylinder(geom, r, h, pos); mk.sphere(geom, r)
120
+ mk.union(geom, [a, b]); mk.difference(geom, a, [b]); mk.intersection(geom, [a, b])
121
+ mk.move(geom, part, (10, 0, 0)); mk.rotate(geom, part, 90, axis='z')
122
+ mk.mirror(geom, part, (1, 0, 0)); mk.array(geom, part, size=(5, 5, 1), displ=(10, 10, 0))
123
+ mk.fillet(geom, part, 0.5); mk.chamfer(geom, part, 0.5) # all edges, or a selection
124
+ mk.partition(geom, part, tool); mk.delete(geom, part)
125
+ plane = mk.workplane(geom, quickz=0)
126
+ mk.circle(plane, 2); mk.extrude(geom, plane, 5); mk.revolve(geom, plane)
127
+ mk.import_(geom, 'part.step')
128
+ mk.feature(geom, 'AnyType', ...) # any other geometry feature
129
+ mk.geometry(model, 2, axisymmetric=True) # r-z half plane: x is r, y is z
130
+ ```
131
+
132
+ Selections by location (`mk.sel`), usable in physics, materials and mesh:
133
+
134
+ ```python
135
+ mk.sel.box(geom, 'boundary', z=0) # a range per axis, or a value
136
+ mk.sel.ball(geom, 'domain', center, r); mk.sel.cylinder(...); mk.sel.disk(...)
137
+ mk.sel.union(geom, 'boundary', [a, b]) # also intersection, difference, complement
138
+ mk.sel.adjacent(geom, domains) # boundaries around a domain selection
139
+ mk.sel.result(geom, feature, 'domain') # what a feature produced
140
+ holes = mk.sel.cumulative(geom, 'holes', 'domain', create=True)
141
+ mk.cylinder(geom, 1, 5, pos, contributeto=holes) # collect from several features
142
+ ```
143
+
144
+ `where='geometry'` makes a selection inside the geometry sequence instead,
145
+ for use as input of a later operation, e.g. `mk.sel.box(geom, 'object',
146
+ x=(20, 40), where='geometry')` to pick whole objects for `mk.delete`. In a
147
+ work plane, selections can pick single corners or edges, e.g. to fillet
148
+ one corner: `mk.fillet(plane, mk.sel.box(plane, 'point', x=1, y=1), 0.3)`.
149
+
150
+ Queries on the plate from the Example section return plain Python values
151
+ and leave nothing in the model:
152
+
153
+ ```python
154
+ mk.sel.entities(geom, bottom) # [3]
155
+ mk.sel.find(geom, 'boundary', x=0) # [1]: the face at x = 0
156
+ mk.measure(geom, 'domain') # 99216.5: volume, approximate where curved
157
+ mk.bounding_box(geom, 'boundary', 3) # {'x': (0.0, 100.0), 'y': ..., 'z': (0.0, 0.0)}
158
+ mk.summary(geom) # counts, voids, bounding box, unit
159
+ mk.sel.neighbors(geom, 'domain', boundary=3) # [1]: the domain beside it
160
+ mk.coordinates(geom, 'boundary', 3) # {1: (0.0, 0.0, 0.0), 3: (0.0, 100.0, 0.0), ...}
161
+ ```
162
+
163
+ Before solving, a check of what COMSOL would get wrong silently or
164
+ vaguely (wrong units, domains without material, conditions that apply
165
+ nowhere, no mesh, physics no study solves):
166
+
167
+ ```python
168
+ problems = [p for p in mk.check(model) if p['severity'] == 'warning']
169
+ ```
170
+
171
+ Before a long solve, its size; while it runs, its progress, read from
172
+ another process (an agent runs it in the background):
173
+
174
+ ```python
175
+ mk.problem_size(model) # degrees of freedom, solver, mesh elements, memory and cores
176
+ mk.log_progress('solve.log') # in the solving script, before loading the model
177
+ mk.progress('/abs/path/solve.log') # elsewhere: percent, task, memory, time steps, alive, CPU
178
+ ```
179
+
180
+ `mk.problem_size` compiles the equations without solving (it needs a
181
+ built mesh) and does not predict memory or time; a direct solver needs
182
+ far more memory than an iterative one. `mk.progress` reads COMSOL's
183
+ progress log and the operating system and judges nothing.
184
+ `help(mk.progress)` has a script that starts a solve in the background
185
+ and how to stop it: on macOS (Linux not tried) end its Python process only,
186
+ `os.kill(pid, signal.SIGTERM)`; on Windows every process `mk.progress`
187
+ lists.
188
+
189
+ Results of the solved
190
+ [example script](https://github.com/elgar328/mphkit/blob/main/examples/plate_with_holes.py),
191
+ over entities or at points, in SI units unless `unit` is given; they
192
+ leave nothing in the model either:
193
+
194
+ ```python
195
+ mk.integral(geom, 'boundary', 'ht.ntflux', selections['hot end'], unit='W') # -2.74: flows in
196
+ mk.average(geom, 'domain', 'T', unit='degC') # 90.2
197
+ mk.minimum(geom, 'domain', 'T', unit='degC', position=True) # (83.4, array([88.0, 20.0, ...])): a hole wall
198
+ mk.value(geom, 'T', [(50, 20, 2.5), (100, 20, 2.5)], unit='degC') # array([89.8, 84.1])
199
+ ```
200
+
201
+ `ht.ntflux` is the flux out of the domain. With several solutions, pass
202
+ `dataset=` (or the study); with time steps or a sweep stored as steps,
203
+ `step=`: a position (`step=10` is the tenth step; a warning tells when
204
+ another step has t = 10 s) or a value (`step={'t': 10}`). A unit that
205
+ does not fit, a point outside the geometry or a geometry changed since
206
+ the solve and not built again raise instead of giving a wrong number;
207
+ solve again after any change, as a geometry changed and then built and
208
+ meshed again is read with the old solution. Global values come from
209
+ MPh: `model.evaluate('expression', 'unit')`, which reads only the last
210
+ value of a sweep stored as an outer loop unless given the sweep's
211
+ dataset.
212
+
213
+ Parametric sweeps that COMSOL stores as an outer loop (around a
214
+ time-dependent or eigenvalue study or several frequencies, or over
215
+ geometry or mesh parameters, or COMSOL's Material and Function Sweeps)
216
+ take `outer=`, e.g. `mk.average(geom, 'domain', 'T', unit='degC',
217
+ outer='all', step='last')` for one value per parameter value, or
218
+ `outer={'Th': '200[degC]'}` for one; `mk.outer_values(geom)` tells
219
+ whether a sweep is one (and lists the values, in SI units), and
220
+ `mk.step_values(geom)` lists the steps' values, eigenfrequencies too
221
+ (`'freq'`, in Hz).
222
+
223
+ Pictures, written to a file:
224
+
225
+ ```python
226
+ mk.image(geom, 'geom.png') # the geometry
227
+ mk.image(geom, 'selection.png', selection, labels=True) # with numbers
228
+ mk.image(geom, 'mesh.png', mesh=True) # element quality
229
+ mk.plot(geom, 'T', 'T.png', unit='degC') # a solved result
230
+ mk.plot(geom, 'T', 'mid.png', unit='degC', z=2.5, view='top') # a slice from above
231
+ ```
232
+
233
+ `mk.plot` also draws a selection only, deformed shapes (`deform=True`),
234
+ fixed colours (`color_range=`, `color_table=`) and the values of a sweep,
235
+ one picture each (`mk.plot(geom, 'T', 'T_{outer}.png', outer='all',
236
+ step='last')`); like the other helpers it leaves nothing in the model.
237
+
238
+ Mesh quality in numbers, after `model.mesh()` (skewness by default, 1 is
239
+ best):
240
+
241
+ ```python
242
+ mk.mesh_quality(geom) # lowest (e.g. 0.065), the 5 worst elements and where, a histogram, COMSOL's messages
243
+ mk.mesh_quality(geom, 'boundary', 3) # the surface elements of boundary 3
244
+ mk.mesh_quality(geom, quality='volcircum') # another of COMSOL's quality measures
245
+ ```
246
+
247
+ The `messages` are what COMSOL reported when building the mesh, e.g. an
248
+ edge much shorter than the element size, with the entities concerned.
249
+
250
+ COMSOL's names, looked up instead of guessed (search first, the full
251
+ lists are long), here on the example script's model with
252
+ `heat = model/'physics'/'heat'`, `mesh = model/'meshes'/'mesh'` and
253
+ `study = model/'studies'/'static'`:
254
+
255
+ ```python
256
+ mk.physics_types(geom, search='heat') # 'HeatTransfer', ...
257
+ mk.feature_types(heat, search='convective') # 'ConvectiveOutflow', then 'HeatFluxBoundary' ({'boundary': 2}) via a choice
258
+ mk.feature_types(mesh) # 'FreeTet', 'Size', ...: what mesh.create() takes
259
+ mk.feature_types(study, search='time') # ..., 'Transient', ...: by name first
260
+ mk.properties(heat, 'HeatFluxBoundary') # descriptions, defaults, choices
261
+ mk.properties(heat/'cooling', search='flux') # and current values
262
+ mk.variables(heat, search='heat flux') # 'ht.ntflux' [W/m^2], on boundaries
263
+ ```
264
+
265
+ Physics features come with the level numbers `create()` takes.
266
+ `mk.properties` also takes geometry, mesh and study features and
267
+ materials, or a type to create there, e.g. `mk.properties(mesh, 'FreeTet')`.
268
+
269
+ Materials from COMSOL's libraries, instead of typing property values:
270
+
271
+ ```python
272
+ mk.materials(search='structural steel') # names, groups, properties
273
+ steel = mk.material(geom, 'Structural steel') # background first: all domains
274
+ water = mk.material(geom, 'Water, liquid', channel) # later ones take a selection
275
+ ```
276
+
277
+ `mk.materials()` lists the basic library and a search looks in all of
278
+ them; `mk.material(..., library='acdc')` takes the others. Each domain
279
+ takes the material added last among those that select it.
280
+
281
+ And a few helpers outside geometry:
282
+
283
+ ```python
284
+ mk.coordinate_system(geom, 'PML', selection=layer) # e.g. perfectly matched layers
285
+ mk.set(mesh_size, hmax=0.5, hgrad=2) # any node or Java object; converts ints and lists
286
+ ```
287
+
288
+ [`examples/plate_with_holes.py`](https://github.com/elgar328/mphkit/blob/main/examples/plate_with_holes.py)
289
+ is a complete script, from geometry to solved results: heat conduction in
290
+ a plate with a row of cooling holes, for one or more holes.
291
+
292
+ ## Limitations
293
+
294
+ - Physics, mesh and study setup, and plots beyond `mk.plot` (arrows,
295
+ streamlines, graphs, animations), stay plain MPh; mphkit looks up the
296
+ names they need and checks them before solving.
297
+ - A parametric sweep that changes the geometry is read over selection
298
+ nodes or all entities, each value in its own geometry, and not drawn;
299
+ a box at fixed coordinates picks what lies there in each value. With
300
+ physics on part of the geometry and the default mesh, the same holds
301
+ for any sweep stored as an outer loop except material sweeps. A
302
+ change that keeps every vertex but numbers the entities otherwise
303
+ goes unnoticed. Batch and cluster sweeps, optimization studies and
304
+ time-dependent studies ended by a stop condition were not tried.
305
+ - `mk.progress` and `mk.problem_size` read memory (and `mk.progress`
306
+ CPU) from macOS and Windows; on Linux some of them are `None` (not
307
+ tried there).
308
+ - No named helpers yet for geometry parts (`PartInstance`), sweeps, cones
309
+ and the other remaining primitives, virtual operations or repair. They
310
+ work through `mk.feature(geom, 'Sweep', ...)`, which handles arguments
311
+ like the named helpers (expressions, lists, nodes as inputs).
312
+
313
+ ## Development
314
+
315
+ ```
316
+ uv run pytest
317
+ ```
318
+
319
+ The tests start COMSOL and build real models. Without a CAD import
320
+ license, the CAD import test is skipped. `uv run pytest -m "not comsol"`
321
+ runs only the tests that need no COMSOL, in seconds.
322
+
323
+ ## License
324
+
325
+ MIT, see [LICENSE](https://github.com/elgar328/mphkit/blob/main/LICENSE).
mphkit-0.3.0/README.md ADDED
@@ -0,0 +1,294 @@
1
+ # mphkit
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/mphkit)](https://pypi.org/project/mphkit/)
4
+ [![Python](https://img.shields.io/python/required-version-toml?tomlFilePath=https://raw.githubusercontent.com/elgar328/mphkit/main/pyproject.toml)](https://pypi.org/project/mphkit/)
5
+ [![License](https://img.shields.io/pypi/l/mphkit)](https://github.com/elgar328/mphkit/blob/main/LICENSE)
6
+ [![COMSOL](https://img.shields.io/badge/COMSOL-6.4-blue)](https://www.comsol.com/)
7
+
8
+ Helpers on top of [MPh](https://github.com/MPh-py/MPh) for COMSOL in
9
+ Python: build geometry, select by location, insert library materials,
10
+ check a model before solving, and read results as numbers or pictures.
11
+ Selections made by location, not entity number, keep working when the
12
+ geometry changes.
13
+
14
+ > [!WARNING]
15
+ > **Early stage.** The API may change at any time, without deprecation
16
+ > warnings.
17
+
18
+ Not affiliated with COMSOL AB.
19
+
20
+ ## Example
21
+
22
+ ```python
23
+ import mph
24
+ import mphkit as mk
25
+
26
+ client = mph.start()
27
+ model = client.create('demo')
28
+ geom = mk.geometry(model, 3, length_unit='mm')
29
+
30
+ plate = mk.block(geom, (100, 100, 10), name='plate')
31
+ hole = mk.cylinder(geom, 5, 10, (50, 50, 0))
32
+ mk.difference(geom, plate, [hole])
33
+ model.build(geom)
34
+
35
+ bottom = mk.sel.box(geom, 'boundary', z=0) # faces on the plane z = 0
36
+
37
+ # plain MPh from here on
38
+ physics = (model/'physics').create('HeatTransfer', geom)
39
+ physics.create('TemperatureBoundary', 2).select(bottom) # 2: boundaries in 3D
40
+ model.save('demo.mph')
41
+ ```
42
+
43
+ Helpers take MPh `Node`s (`mk.geometry` the model, `mk.set` also Java
44
+ objects), and those that create something return one, so mphkit and MPh
45
+ mix freely. Physics, mesh and study stay plain MPh (or the COMSOL Java
46
+ API through `node.java`); mphkit looks up the COMSOL names they need.
47
+
48
+ `print(mphkit.__doc__)` shows the workflow as one script, the rules and
49
+ an index of every helper; `help(mk.<name>)` has the details of one,
50
+ `help(mk.sel)` those of the selections. Point an AI assistant to
51
+ `print(mphkit.__doc__)` first.
52
+
53
+ ## Requirements
54
+
55
+ - COMSOL Multiphysics with a license, installed where MPh can find it
56
+ (see the [MPh documentation](https://mph.readthedocs.io)).
57
+ - Python 3.10 or newer, MPh 1.4 or newer (below 2: mphkit uses some of
58
+ its internals).
59
+ - Importing CAD files (`mk.import_` with STEP, IGES, ...) needs a license
60
+ for CAD import (CAD Import Module, Design Module or a LiveLink).
61
+ Everything else needs COMSOL only.
62
+
63
+ mphkit is developed and tested with COMSOL 6.4 and MPh 1.4. Feature types,
64
+ property names and selection behavior can differ between COMSOL versions,
65
+ so other versions may need adjustments; reports are welcome. Checked on
66
+ macOS and Windows; it should run wherever MPh runs. The name lookups read
67
+ COMSOL's code-completion data in the installation (`data/completion`),
68
+ which COMSOL does not document; they were checked with COMSOL 6.4 on
69
+ macOS and Windows. A missing or changed catalogue raises an error rather
70
+ than giving wrong names.
71
+
72
+ ## Installation
73
+
74
+ ```
75
+ pip install mphkit
76
+ ```
77
+
78
+ or `uv add mphkit` in a uv project.
79
+
80
+ What changed between versions is listed in
81
+ [CHANGELOG.md](https://github.com/elgar328/mphkit/blob/main/CHANGELOG.md).
82
+
83
+ ## What it covers
84
+
85
+ Geometry, in 3D, 2D and in work planes:
86
+
87
+ ```python
88
+ mk.block(geom, (10, 10, 5)); mk.cylinder(geom, r, h, pos); mk.sphere(geom, r)
89
+ mk.union(geom, [a, b]); mk.difference(geom, a, [b]); mk.intersection(geom, [a, b])
90
+ mk.move(geom, part, (10, 0, 0)); mk.rotate(geom, part, 90, axis='z')
91
+ mk.mirror(geom, part, (1, 0, 0)); mk.array(geom, part, size=(5, 5, 1), displ=(10, 10, 0))
92
+ mk.fillet(geom, part, 0.5); mk.chamfer(geom, part, 0.5) # all edges, or a selection
93
+ mk.partition(geom, part, tool); mk.delete(geom, part)
94
+ plane = mk.workplane(geom, quickz=0)
95
+ mk.circle(plane, 2); mk.extrude(geom, plane, 5); mk.revolve(geom, plane)
96
+ mk.import_(geom, 'part.step')
97
+ mk.feature(geom, 'AnyType', ...) # any other geometry feature
98
+ mk.geometry(model, 2, axisymmetric=True) # r-z half plane: x is r, y is z
99
+ ```
100
+
101
+ Selections by location (`mk.sel`), usable in physics, materials and mesh:
102
+
103
+ ```python
104
+ mk.sel.box(geom, 'boundary', z=0) # a range per axis, or a value
105
+ mk.sel.ball(geom, 'domain', center, r); mk.sel.cylinder(...); mk.sel.disk(...)
106
+ mk.sel.union(geom, 'boundary', [a, b]) # also intersection, difference, complement
107
+ mk.sel.adjacent(geom, domains) # boundaries around a domain selection
108
+ mk.sel.result(geom, feature, 'domain') # what a feature produced
109
+ holes = mk.sel.cumulative(geom, 'holes', 'domain', create=True)
110
+ mk.cylinder(geom, 1, 5, pos, contributeto=holes) # collect from several features
111
+ ```
112
+
113
+ `where='geometry'` makes a selection inside the geometry sequence instead,
114
+ for use as input of a later operation, e.g. `mk.sel.box(geom, 'object',
115
+ x=(20, 40), where='geometry')` to pick whole objects for `mk.delete`. In a
116
+ work plane, selections can pick single corners or edges, e.g. to fillet
117
+ one corner: `mk.fillet(plane, mk.sel.box(plane, 'point', x=1, y=1), 0.3)`.
118
+
119
+ Queries on the plate from the Example section return plain Python values
120
+ and leave nothing in the model:
121
+
122
+ ```python
123
+ mk.sel.entities(geom, bottom) # [3]
124
+ mk.sel.find(geom, 'boundary', x=0) # [1]: the face at x = 0
125
+ mk.measure(geom, 'domain') # 99216.5: volume, approximate where curved
126
+ mk.bounding_box(geom, 'boundary', 3) # {'x': (0.0, 100.0), 'y': ..., 'z': (0.0, 0.0)}
127
+ mk.summary(geom) # counts, voids, bounding box, unit
128
+ mk.sel.neighbors(geom, 'domain', boundary=3) # [1]: the domain beside it
129
+ mk.coordinates(geom, 'boundary', 3) # {1: (0.0, 0.0, 0.0), 3: (0.0, 100.0, 0.0), ...}
130
+ ```
131
+
132
+ Before solving, a check of what COMSOL would get wrong silently or
133
+ vaguely (wrong units, domains without material, conditions that apply
134
+ nowhere, no mesh, physics no study solves):
135
+
136
+ ```python
137
+ problems = [p for p in mk.check(model) if p['severity'] == 'warning']
138
+ ```
139
+
140
+ Before a long solve, its size; while it runs, its progress, read from
141
+ another process (an agent runs it in the background):
142
+
143
+ ```python
144
+ mk.problem_size(model) # degrees of freedom, solver, mesh elements, memory and cores
145
+ mk.log_progress('solve.log') # in the solving script, before loading the model
146
+ mk.progress('/abs/path/solve.log') # elsewhere: percent, task, memory, time steps, alive, CPU
147
+ ```
148
+
149
+ `mk.problem_size` compiles the equations without solving (it needs a
150
+ built mesh) and does not predict memory or time; a direct solver needs
151
+ far more memory than an iterative one. `mk.progress` reads COMSOL's
152
+ progress log and the operating system and judges nothing.
153
+ `help(mk.progress)` has a script that starts a solve in the background
154
+ and how to stop it: on macOS (Linux not tried) end its Python process only,
155
+ `os.kill(pid, signal.SIGTERM)`; on Windows every process `mk.progress`
156
+ lists.
157
+
158
+ Results of the solved
159
+ [example script](https://github.com/elgar328/mphkit/blob/main/examples/plate_with_holes.py),
160
+ over entities or at points, in SI units unless `unit` is given; they
161
+ leave nothing in the model either:
162
+
163
+ ```python
164
+ mk.integral(geom, 'boundary', 'ht.ntflux', selections['hot end'], unit='W') # -2.74: flows in
165
+ mk.average(geom, 'domain', 'T', unit='degC') # 90.2
166
+ mk.minimum(geom, 'domain', 'T', unit='degC', position=True) # (83.4, array([88.0, 20.0, ...])): a hole wall
167
+ mk.value(geom, 'T', [(50, 20, 2.5), (100, 20, 2.5)], unit='degC') # array([89.8, 84.1])
168
+ ```
169
+
170
+ `ht.ntflux` is the flux out of the domain. With several solutions, pass
171
+ `dataset=` (or the study); with time steps or a sweep stored as steps,
172
+ `step=`: a position (`step=10` is the tenth step; a warning tells when
173
+ another step has t = 10 s) or a value (`step={'t': 10}`). A unit that
174
+ does not fit, a point outside the geometry or a geometry changed since
175
+ the solve and not built again raise instead of giving a wrong number;
176
+ solve again after any change, as a geometry changed and then built and
177
+ meshed again is read with the old solution. Global values come from
178
+ MPh: `model.evaluate('expression', 'unit')`, which reads only the last
179
+ value of a sweep stored as an outer loop unless given the sweep's
180
+ dataset.
181
+
182
+ Parametric sweeps that COMSOL stores as an outer loop (around a
183
+ time-dependent or eigenvalue study or several frequencies, or over
184
+ geometry or mesh parameters, or COMSOL's Material and Function Sweeps)
185
+ take `outer=`, e.g. `mk.average(geom, 'domain', 'T', unit='degC',
186
+ outer='all', step='last')` for one value per parameter value, or
187
+ `outer={'Th': '200[degC]'}` for one; `mk.outer_values(geom)` tells
188
+ whether a sweep is one (and lists the values, in SI units), and
189
+ `mk.step_values(geom)` lists the steps' values, eigenfrequencies too
190
+ (`'freq'`, in Hz).
191
+
192
+ Pictures, written to a file:
193
+
194
+ ```python
195
+ mk.image(geom, 'geom.png') # the geometry
196
+ mk.image(geom, 'selection.png', selection, labels=True) # with numbers
197
+ mk.image(geom, 'mesh.png', mesh=True) # element quality
198
+ mk.plot(geom, 'T', 'T.png', unit='degC') # a solved result
199
+ mk.plot(geom, 'T', 'mid.png', unit='degC', z=2.5, view='top') # a slice from above
200
+ ```
201
+
202
+ `mk.plot` also draws a selection only, deformed shapes (`deform=True`),
203
+ fixed colours (`color_range=`, `color_table=`) and the values of a sweep,
204
+ one picture each (`mk.plot(geom, 'T', 'T_{outer}.png', outer='all',
205
+ step='last')`); like the other helpers it leaves nothing in the model.
206
+
207
+ Mesh quality in numbers, after `model.mesh()` (skewness by default, 1 is
208
+ best):
209
+
210
+ ```python
211
+ mk.mesh_quality(geom) # lowest (e.g. 0.065), the 5 worst elements and where, a histogram, COMSOL's messages
212
+ mk.mesh_quality(geom, 'boundary', 3) # the surface elements of boundary 3
213
+ mk.mesh_quality(geom, quality='volcircum') # another of COMSOL's quality measures
214
+ ```
215
+
216
+ The `messages` are what COMSOL reported when building the mesh, e.g. an
217
+ edge much shorter than the element size, with the entities concerned.
218
+
219
+ COMSOL's names, looked up instead of guessed (search first, the full
220
+ lists are long), here on the example script's model with
221
+ `heat = model/'physics'/'heat'`, `mesh = model/'meshes'/'mesh'` and
222
+ `study = model/'studies'/'static'`:
223
+
224
+ ```python
225
+ mk.physics_types(geom, search='heat') # 'HeatTransfer', ...
226
+ mk.feature_types(heat, search='convective') # 'ConvectiveOutflow', then 'HeatFluxBoundary' ({'boundary': 2}) via a choice
227
+ mk.feature_types(mesh) # 'FreeTet', 'Size', ...: what mesh.create() takes
228
+ mk.feature_types(study, search='time') # ..., 'Transient', ...: by name first
229
+ mk.properties(heat, 'HeatFluxBoundary') # descriptions, defaults, choices
230
+ mk.properties(heat/'cooling', search='flux') # and current values
231
+ mk.variables(heat, search='heat flux') # 'ht.ntflux' [W/m^2], on boundaries
232
+ ```
233
+
234
+ Physics features come with the level numbers `create()` takes.
235
+ `mk.properties` also takes geometry, mesh and study features and
236
+ materials, or a type to create there, e.g. `mk.properties(mesh, 'FreeTet')`.
237
+
238
+ Materials from COMSOL's libraries, instead of typing property values:
239
+
240
+ ```python
241
+ mk.materials(search='structural steel') # names, groups, properties
242
+ steel = mk.material(geom, 'Structural steel') # background first: all domains
243
+ water = mk.material(geom, 'Water, liquid', channel) # later ones take a selection
244
+ ```
245
+
246
+ `mk.materials()` lists the basic library and a search looks in all of
247
+ them; `mk.material(..., library='acdc')` takes the others. Each domain
248
+ takes the material added last among those that select it.
249
+
250
+ And a few helpers outside geometry:
251
+
252
+ ```python
253
+ mk.coordinate_system(geom, 'PML', selection=layer) # e.g. perfectly matched layers
254
+ mk.set(mesh_size, hmax=0.5, hgrad=2) # any node or Java object; converts ints and lists
255
+ ```
256
+
257
+ [`examples/plate_with_holes.py`](https://github.com/elgar328/mphkit/blob/main/examples/plate_with_holes.py)
258
+ is a complete script, from geometry to solved results: heat conduction in
259
+ a plate with a row of cooling holes, for one or more holes.
260
+
261
+ ## Limitations
262
+
263
+ - Physics, mesh and study setup, and plots beyond `mk.plot` (arrows,
264
+ streamlines, graphs, animations), stay plain MPh; mphkit looks up the
265
+ names they need and checks them before solving.
266
+ - A parametric sweep that changes the geometry is read over selection
267
+ nodes or all entities, each value in its own geometry, and not drawn;
268
+ a box at fixed coordinates picks what lies there in each value. With
269
+ physics on part of the geometry and the default mesh, the same holds
270
+ for any sweep stored as an outer loop except material sweeps. A
271
+ change that keeps every vertex but numbers the entities otherwise
272
+ goes unnoticed. Batch and cluster sweeps, optimization studies and
273
+ time-dependent studies ended by a stop condition were not tried.
274
+ - `mk.progress` and `mk.problem_size` read memory (and `mk.progress`
275
+ CPU) from macOS and Windows; on Linux some of them are `None` (not
276
+ tried there).
277
+ - No named helpers yet for geometry parts (`PartInstance`), sweeps, cones
278
+ and the other remaining primitives, virtual operations or repair. They
279
+ work through `mk.feature(geom, 'Sweep', ...)`, which handles arguments
280
+ like the named helpers (expressions, lists, nodes as inputs).
281
+
282
+ ## Development
283
+
284
+ ```
285
+ uv run pytest
286
+ ```
287
+
288
+ The tests start COMSOL and build real models. Without a CAD import
289
+ license, the CAD import test is skipped. `uv run pytest -m "not comsol"`
290
+ runs only the tests that need no COMSOL, in seconds.
291
+
292
+ ## License
293
+
294
+ MIT, see [LICENSE](https://github.com/elgar328/mphkit/blob/main/LICENSE).
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "mphkit"
3
- version = "0.2.0"
4
- description = "Helpers on top of MPh for building COMSOL geometries and geometry-based selections in Python."
3
+ version = "0.3.0"
4
+ description = "Helpers on top of MPh for COMSOL in Python: build geometry, select by location, insert library materials, check a model before solving, and read results as numbers or pictures."
5
5
  readme = "README.md"
6
6
  license = "MIT"
7
7
  license-files = ["LICENSE"]
@@ -16,7 +16,7 @@ keywords = [
16
16
  ]
17
17
  requires-python = ">=3.10"
18
18
  dependencies = [
19
- "mph>=1.4",
19
+ "mph>=1.4,<2",
20
20
  "numpy",
21
21
  ]
22
22
  classifiers = [
@@ -57,3 +57,6 @@ build-backend = "uv_build"
57
57
 
58
58
  [tool.pytest.ini_options]
59
59
  testpaths = ["tests"]
60
+ addopts = ["--strict-markers"]
61
+ filterwarnings = ["error::mphkit.StepWarning"]
62
+ markers = ["comsol: needs COMSOL (running or installed); set by conftest.py"]
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "mphkit"
3
- version = "0.2.0"
4
- description = "Helpers on top of MPh for building COMSOL geometries and geometry-based selections in Python."
3
+ version = "0.3.0"
4
+ description = "Helpers on top of MPh for COMSOL in Python: build geometry, select by location, insert library materials, check a model before solving, and read results as numbers or pictures."
5
5
  readme = "README.md"
6
6
  authors = [{ name = "elgar328" }]
7
7
  license = "MIT"
@@ -9,7 +9,7 @@ license-files = ["LICENSE"]
9
9
  keywords = ["comsol", "mph", "multiphysics", "geometry", "selection",
10
10
  "simulation", "fem"]
11
11
  requires-python = ">=3.10"
12
- dependencies = ["mph>=1.4", "numpy"]
12
+ dependencies = ["mph>=1.4,<2", "numpy"]
13
13
  classifiers = [
14
14
  "Development Status :: 3 - Alpha",
15
15
  "Intended Audience :: Science/Research",
@@ -45,3 +45,9 @@ build-backend = "uv_build"
45
45
 
46
46
  [tool.pytest.ini_options]
47
47
  testpaths = ["tests"]
48
+ addopts = ["--strict-markers"]
49
+ # a test that gives a step or outer value by number on purpose says so
50
+ filterwarnings = ["error::mphkit.StepWarning"]
51
+ markers = [
52
+ "comsol: needs COMSOL (running or installed); set by conftest.py",
53
+ ]