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.
- mphkit-0.3.0/PKG-INFO +325 -0
- mphkit-0.3.0/README.md +294 -0
- {mphkit-0.2.0 → mphkit-0.3.0}/pyproject.toml +6 -3
- {mphkit-0.2.0 → mphkit-0.3.0}/pyproject.toml.orig +9 -3
- mphkit-0.3.0/src/mphkit/__init__.py +176 -0
- mphkit-0.3.0/src/mphkit/_catalog.py +1158 -0
- mphkit-0.3.0/src/mphkit/_check.py +635 -0
- {mphkit-0.2.0 → mphkit-0.3.0}/src/mphkit/_comsol.py +289 -34
- mphkit-0.3.0/src/mphkit/_datasets.py +408 -0
- {mphkit-0.2.0 → mphkit-0.3.0}/src/mphkit/_expr.py +2 -5
- mphkit-0.3.0/src/mphkit/_hints.py +490 -0
- {mphkit-0.2.0 → mphkit-0.3.0}/src/mphkit/_image.py +174 -60
- mphkit-0.3.0/src/mphkit/_materials.py +437 -0
- {mphkit-0.2.0 → mphkit-0.3.0}/src/mphkit/_measure.py +25 -16
- mphkit-0.3.0/src/mphkit/_mesh.py +425 -0
- mphkit-0.3.0/src/mphkit/_plot.py +680 -0
- {mphkit-0.2.0 → mphkit-0.3.0}/src/mphkit/_props.py +10 -5
- mphkit-0.3.0/src/mphkit/_results.py +872 -0
- {mphkit-0.2.0 → mphkit-0.3.0}/src/mphkit/_sel.py +122 -31
- mphkit-0.3.0/src/mphkit/_solve.py +954 -0
- mphkit-0.3.0/src/mphkit/_sweep.py +1868 -0
- mphkit-0.3.0/src/mphkit/_winproc.py +332 -0
- mphkit-0.3.0/src/mphkit/errors.py +17 -0
- {mphkit-0.2.0 → mphkit-0.3.0}/src/mphkit/geometry.py +57 -13
- mphkit-0.2.0/PKG-INFO +0 -193
- mphkit-0.2.0/README.md +0 -162
- mphkit-0.2.0/src/mphkit/__init__.py +0 -117
- mphkit-0.2.0/src/mphkit/_hints.py +0 -274
- mphkit-0.2.0/src/mphkit/errors.py +0 -5
- {mphkit-0.2.0 → mphkit-0.3.0}/LICENSE +0 -0
- {mphkit-0.2.0 → mphkit-0.3.0}/src/mphkit/py.typed +0 -0
- {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
|
+
[](https://pypi.org/project/mphkit/)
|
|
35
|
+
[](https://pypi.org/project/mphkit/)
|
|
36
|
+
[](https://github.com/elgar328/mphkit/blob/main/LICENSE)
|
|
37
|
+
[](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
|
+
[](https://pypi.org/project/mphkit/)
|
|
4
|
+
[](https://pypi.org/project/mphkit/)
|
|
5
|
+
[](https://github.com/elgar328/mphkit/blob/main/LICENSE)
|
|
6
|
+
[](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.
|
|
4
|
-
description = "Helpers on top of MPh for
|
|
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.
|
|
4
|
-
description = "Helpers on top of MPh for
|
|
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
|
+
]
|