mphkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mphkit/__init__.py +23 -0
- mphkit/_comsol.py +719 -0
- mphkit/_expr.py +37 -0
- mphkit/_measure.py +102 -0
- mphkit/_props.py +62 -0
- mphkit/_sel.py +487 -0
- mphkit/errors.py +5 -0
- mphkit/geometry.py +537 -0
- mphkit/py.typed +0 -0
- mphkit/sel.py +37 -0
- mphkit-0.1.0.dist-info/METADATA +174 -0
- mphkit-0.1.0.dist-info/RECORD +14 -0
- mphkit-0.1.0.dist-info/WHEEL +4 -0
- mphkit-0.1.0.dist-info/licenses/LICENSE +21 -0
mphkit/geometry.py
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
"""Geometry creation, geometry features, and coordinate systems."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import numpy
|
|
5
|
+
from mph import Model, Node
|
|
6
|
+
from mph.node import escape
|
|
7
|
+
|
|
8
|
+
from . import _comsol
|
|
9
|
+
from ._comsol import WorkPlaneNode
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def geometry(model: Model, dim: int = 3, *, length_unit: str = None,
|
|
13
|
+
name: str = None) -> Node:
|
|
14
|
+
"""
|
|
15
|
+
Creates a new component with a geometry of dimension `dim`.
|
|
16
|
+
|
|
17
|
+
The component is created explicitly, so this also works in models that
|
|
18
|
+
already have components. `length_unit` is, for example, `'mm'`; plain
|
|
19
|
+
numbers given to other helpers are then interpreted in that unit.
|
|
20
|
+
Returns the geometry node.
|
|
21
|
+
"""
|
|
22
|
+
java = model.java
|
|
23
|
+
taken = _comsol.labels(java.geom())
|
|
24
|
+
if name is not None:
|
|
25
|
+
_comsol.pick_label(name, name, taken)
|
|
26
|
+
ctag = str(java.component().uniquetag('comp'))
|
|
27
|
+
component = java.component().create(ctag, True)
|
|
28
|
+
gtag = str(java.geom().uniquetag('geom'))
|
|
29
|
+
geom = component.geom().create(gtag, dim)
|
|
30
|
+
label = _comsol.pick_label(name, str(geom.label()), taken)
|
|
31
|
+
geom.label(label)
|
|
32
|
+
if length_unit is not None:
|
|
33
|
+
geom.lengthUnit(length_unit)
|
|
34
|
+
node = model/'geometries'/escape(label)
|
|
35
|
+
_comsol.check_tag(node, gtag)
|
|
36
|
+
return node
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def component_of(geom: Node) -> Node:
|
|
40
|
+
"""
|
|
41
|
+
Returns the component node that contains the geometry (does not create).
|
|
42
|
+
|
|
43
|
+
Useful for the parts of a model that live under the component, e.g.
|
|
44
|
+
`mk.component_of(geom).java.coordSystem()`. For coordinate systems use
|
|
45
|
+
`coordinate_system()`.
|
|
46
|
+
"""
|
|
47
|
+
java = _comsol.component_of(geom)
|
|
48
|
+
node = geom.model/'components'/escape(str(java.label()))
|
|
49
|
+
_comsol.check_tag(node, str(java.tag()))
|
|
50
|
+
return node
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def coordinate_system(geom: Node, type: str, /, *, selection: Node = None,
|
|
54
|
+
name: str = None, **properties) -> Node:
|
|
55
|
+
"""
|
|
56
|
+
Creates a coordinate system of `type` for the geometry.
|
|
57
|
+
|
|
58
|
+
For example a perfectly matched layer:
|
|
59
|
+
`coordinate_system(geom, 'PML', selection=pml, stretchingType='rational')`.
|
|
60
|
+
Other types include `'InfiniteElement'`, `'AbsorbingLayer'` and
|
|
61
|
+
`'Scaling'`, which take a domain `selection`, and `'Rotated'`,
|
|
62
|
+
`'Cylindrical'`, `'Spherical'` or `'Boundary'`, which do not. Keyword
|
|
63
|
+
arguments are COMSOL property names. Returns the node under
|
|
64
|
+
`model/'coordinates'`.
|
|
65
|
+
|
|
66
|
+
Plain MPh works as well when the geometry tag comes first,
|
|
67
|
+
`(model/'coordinates').create(geom.tag(), 'PML')`. Without it, COMSOL
|
|
68
|
+
leaves a broken node that makes every later physics node fail.
|
|
69
|
+
"""
|
|
70
|
+
model = geom.model
|
|
71
|
+
container = _comsol.component_of(geom).coordSystem()
|
|
72
|
+
everything = model.java.coordSystem()
|
|
73
|
+
tag, label = _comsol.create_java(container, 'coordinates', type, name,
|
|
74
|
+
_comsol.labels(everything),
|
|
75
|
+
tags=everything, args=(geom.tag(),))
|
|
76
|
+
node = model/'coordinates'/escape(label)
|
|
77
|
+
try:
|
|
78
|
+
_comsol.check_tag(node, tag)
|
|
79
|
+
java = container.get(tag)
|
|
80
|
+
_comsol.set_properties(java, properties)
|
|
81
|
+
if selection is not None:
|
|
82
|
+
try:
|
|
83
|
+
target = java.selection()
|
|
84
|
+
except Exception:
|
|
85
|
+
raise ValueError(f'A "{type}" coordinate system has no '
|
|
86
|
+
'selection.') from None
|
|
87
|
+
chosen = _comsol.check_selection(geom, selection)
|
|
88
|
+
if list(chosen.dimension()) != list(target.dimension()):
|
|
89
|
+
raise ValueError(f'Selection "{selection}" is not at the '
|
|
90
|
+
f'level a "{type}" coordinate system '
|
|
91
|
+
'takes.')
|
|
92
|
+
node.select(selection)
|
|
93
|
+
except Exception:
|
|
94
|
+
container.remove(tag)
|
|
95
|
+
raise
|
|
96
|
+
return node
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def feature(parent: Node, type: str, /, *, name: str = None,
|
|
100
|
+
**properties) -> Node:
|
|
101
|
+
"""
|
|
102
|
+
Creates a geometry feature of any COMSOL `type` and sets its properties.
|
|
103
|
+
|
|
104
|
+
`parent` is a geometry or a work plane. Keyword arguments are COMSOL
|
|
105
|
+
property names. Input selections (such as `input` and `input2` of a
|
|
106
|
+
Difference) accept geometry nodes, names, tags, or lists of those, or
|
|
107
|
+
one selection: made with `sel.*(..., where='geometry')` (also at the
|
|
108
|
+
`'object'` level), made in the work plane itself (`sel.box(plane,
|
|
109
|
+
...)`), or `sel.cumulative()`. Lists that
|
|
110
|
+
mix numbers and expressions are converted for COMSOL. Arguments that
|
|
111
|
+
are `None` are skipped.
|
|
112
|
+
|
|
113
|
+
If setting a property fails, the new feature is removed again and the
|
|
114
|
+
error is raised. Returns the feature node.
|
|
115
|
+
"""
|
|
116
|
+
container = _comsol.feature_container(parent)
|
|
117
|
+
workplane = _comsol.is_workplane(parent.java)
|
|
118
|
+
taken = _comsol.labels(container)
|
|
119
|
+
if (type.endswith('Selection')
|
|
120
|
+
or properties.get('selresult') in (True, 'on')):
|
|
121
|
+
taken |= _comsol.selection_labels(parent.model)
|
|
122
|
+
tag, label = _comsol.create_java(container, 'geometries', type, name,
|
|
123
|
+
taken)
|
|
124
|
+
cls = WorkPlaneNode if workplane or type == 'WorkPlane' else Node
|
|
125
|
+
node = _comsol.child_node(parent, label, cls)
|
|
126
|
+
try:
|
|
127
|
+
_comsol.check_tag(node, tag)
|
|
128
|
+
java = container.get(tag)
|
|
129
|
+
_comsol.set_properties(java, properties, parent, container)
|
|
130
|
+
except Exception:
|
|
131
|
+
container.remove(tag)
|
|
132
|
+
raise
|
|
133
|
+
return node
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
#############################
|
|
137
|
+
# Primitives and operations #
|
|
138
|
+
#############################
|
|
139
|
+
|
|
140
|
+
def block(geom: Node, /, size=None, pos=None, *, name: str = None,
|
|
141
|
+
**properties) -> Node:
|
|
142
|
+
"""
|
|
143
|
+
Creates a Block. `base='center'` centers it on `pos`.
|
|
144
|
+
|
|
145
|
+
Layers, e.g. a perfectly matched layer, are shells on chosen faces:
|
|
146
|
+
`layername=['pml'], layer=[5], layertop=True, layerbottom=False`. The
|
|
147
|
+
faces are `layerleft`/`layerright` (−x/+x), `layerfront`/`layerback`
|
|
148
|
+
(−y/+y) and `layerbottom`/`layertop` (−z/+z); only `layerbottom` is on
|
|
149
|
+
by default. Several layers stack from the outer face inward in the
|
|
150
|
+
order of `layername`. Select them with `sel.layer()`.
|
|
151
|
+
"""
|
|
152
|
+
return feature(geom, 'Block', name=name, size=size, pos=pos,
|
|
153
|
+
**properties)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def cylinder(geom: Node, /, r=None, h=None, pos=None, *, name: str = None,
|
|
157
|
+
**properties) -> Node:
|
|
158
|
+
"""
|
|
159
|
+
Creates a Cylinder with radius `r` and height `h`.
|
|
160
|
+
|
|
161
|
+
The cylinder stands on `pos` along the z axis; COMSOL's `axistype` and
|
|
162
|
+
`axis` properties give another direction, e.g. `axistype='x'`.
|
|
163
|
+
"""
|
|
164
|
+
return feature(geom, 'Cylinder', name=name, r=r, h=h, pos=pos,
|
|
165
|
+
**properties)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def sphere(geom: Node, /, r=None, pos=None, *, name: str = None,
|
|
169
|
+
**properties) -> Node:
|
|
170
|
+
"""Creates a Sphere with radius `r` centered at `pos`."""
|
|
171
|
+
return feature(geom, 'Sphere', name=name, r=r, pos=pos, **properties)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def point(geom: Node, /, p, *, name: str = None, **properties) -> Node:
|
|
175
|
+
"""Creates a Point at coordinates `p`."""
|
|
176
|
+
return feature(geom, 'Point', name=name, p=p, **properties)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def union(geom: Node, /, input, *, name: str = None, **properties) -> Node:
|
|
180
|
+
"""Creates a Union of the `input` objects."""
|
|
181
|
+
return feature(geom, 'Union', name=name, input=input, **properties)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def difference(geom: Node, /, input, input2, *, name: str = None,
|
|
185
|
+
**properties) -> Node:
|
|
186
|
+
"""Creates a Difference: `input` objects minus `input2` objects."""
|
|
187
|
+
return feature(geom, 'Difference', name=name, input=input,
|
|
188
|
+
input2=input2, **properties)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def rigid_transform(geom: Node, /, input, *, name: str = None,
|
|
192
|
+
**properties) -> Node:
|
|
193
|
+
"""
|
|
194
|
+
Creates a Rigid Transform (move and rotate) of the `input` objects.
|
|
195
|
+
|
|
196
|
+
For example `displ=(dx, dy, dz)`, `specify='eulerang'`,
|
|
197
|
+
`eulerang=(180, 90, 0)`.
|
|
198
|
+
"""
|
|
199
|
+
return feature(geom, 'RigidTransform', name=name, input=input,
|
|
200
|
+
**properties)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def intersection(parent: Node, /, input, *, name: str = None,
|
|
204
|
+
**properties) -> Node:
|
|
205
|
+
"""Creates an Intersection: the part the `input` objects share."""
|
|
206
|
+
return feature(parent, 'Intersection', name=name, input=input,
|
|
207
|
+
**properties)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def delete(parent: Node, /, input, *, name: str = None,
|
|
211
|
+
**properties) -> Node:
|
|
212
|
+
"""
|
|
213
|
+
Creates a Delete of the `input` objects.
|
|
214
|
+
|
|
215
|
+
`input` may also be one selection: at the `'object'` level or a
|
|
216
|
+
cumulative selection, whole objects are deleted; a domain or boundary
|
|
217
|
+
selection made with `sel.*(..., where='geometry')` deletes just those.
|
|
218
|
+
In a work plane, use a selection made in that plane.
|
|
219
|
+
"""
|
|
220
|
+
return feature(parent, 'Delete', name=name, input=input, **properties)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
##############
|
|
224
|
+
# Transforms #
|
|
225
|
+
##############
|
|
226
|
+
|
|
227
|
+
def array(parent: Node, /, input, *, size, displ, name: str = None,
|
|
228
|
+
**properties) -> Node:
|
|
229
|
+
"""
|
|
230
|
+
Creates an Array of copies of the `input` objects.
|
|
231
|
+
|
|
232
|
+
`size` is the number of copies per axis, e.g. `size=(5, 5, 1)`, or a
|
|
233
|
+
single number (or expression) for a linear array along `displ`.
|
|
234
|
+
`displ` is the spacing, e.g. `displ=(10, 10, 0)`. Both are
|
|
235
|
+
keyword-only so they cannot be swapped by accident. `size` becomes
|
|
236
|
+
COMSOL's `fullsize` or, for a linear array, `linearsize` with
|
|
237
|
+
`type='linear'`. Works in a work plane as well.
|
|
238
|
+
"""
|
|
239
|
+
if isinstance(size, bool):
|
|
240
|
+
raise TypeError(f'size must be a number or one per axis, not {size!r}.')
|
|
241
|
+
_comsol.check_vector(parent, 'displ', displ)
|
|
242
|
+
if isinstance(size, (list, tuple, numpy.ndarray)):
|
|
243
|
+
_comsol.check_vector(parent, 'size', size)
|
|
244
|
+
properties['fullsize'] = size
|
|
245
|
+
else:
|
|
246
|
+
properties.setdefault('type', 'linear')
|
|
247
|
+
properties['linearsize'] = size
|
|
248
|
+
return feature(parent, 'Array', name=name, input=input, displ=displ,
|
|
249
|
+
**properties)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def move(parent: Node, /, input, displ, *, name: str = None,
|
|
253
|
+
**properties) -> Node:
|
|
254
|
+
"""
|
|
255
|
+
Creates a Move of the `input` objects by `displ`, e.g. `(0, 0, 5)`.
|
|
256
|
+
|
|
257
|
+
A component given as a list makes several copies, e.g.
|
|
258
|
+
`displ=([10, 20], 0, 0)`. Pass `keep=True` to keep the originals.
|
|
259
|
+
"""
|
|
260
|
+
_comsol.check_vector(parent, 'displ', displ)
|
|
261
|
+
for axis, value in zip('xyz', displ):
|
|
262
|
+
properties[f'displ{axis}'] = value
|
|
263
|
+
return feature(parent, 'Move', name=name, input=input, **properties)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def rotate(parent: Node, /, input, rot, *, pos=None, axis=None,
|
|
267
|
+
name: str = None, **properties) -> Node:
|
|
268
|
+
"""
|
|
269
|
+
Creates a Rotate of the `input` objects by `rot` degrees.
|
|
270
|
+
|
|
271
|
+
A list of angles makes several copies. `pos` is a point on the axis.
|
|
272
|
+
In 3D, `axis` is `'x'`, `'y'`, `'z'` (default) or a direction vector;
|
|
273
|
+
2D rotations (also in a work plane) have no axis.
|
|
274
|
+
"""
|
|
275
|
+
if pos is not None:
|
|
276
|
+
_comsol.check_vector(parent, 'pos', pos)
|
|
277
|
+
if axis is not None:
|
|
278
|
+
if _comsol.parent_dim(parent) == 2:
|
|
279
|
+
raise ValueError('A 2D rotation has no axis; leave out axis.')
|
|
280
|
+
if not isinstance(axis, str):
|
|
281
|
+
_comsol.check_vector(parent, 'axis', axis)
|
|
282
|
+
properties.update(_comsol.axis_properties(axis))
|
|
283
|
+
return feature(parent, 'Rotate', name=name, input=input, rot=rot,
|
|
284
|
+
pos=pos, **properties)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def mirror(parent: Node, /, input, axis, *, pos=None, name: str = None,
|
|
288
|
+
**properties) -> Node:
|
|
289
|
+
"""
|
|
290
|
+
Creates a Mirror of the `input` objects.
|
|
291
|
+
|
|
292
|
+
`axis` is the **normal** of the mirror plane (3D) or line (2D), as in
|
|
293
|
+
COMSOL: `axis=(1, 0, 0)` mirrors in the plane x = 0, so x changes
|
|
294
|
+
sign. `pos` is a point on the plane. Pass `keep=True` to keep the
|
|
295
|
+
originals.
|
|
296
|
+
"""
|
|
297
|
+
_comsol.check_vector(parent, 'axis', axis)
|
|
298
|
+
if pos is not None:
|
|
299
|
+
_comsol.check_vector(parent, 'pos', pos)
|
|
300
|
+
return feature(parent, 'Mirror', name=name, input=input, axis=axis,
|
|
301
|
+
pos=pos, **properties)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def revolve(geom: Node, /, input, angle=None, *, pos=None, axis=None,
|
|
305
|
+
name: str = None, **properties) -> Node:
|
|
306
|
+
"""
|
|
307
|
+
Creates a Revolve of `input`, usually a work plane, in a 3D geometry.
|
|
308
|
+
|
|
309
|
+
`angle` is left out for a full turn, one value in degrees (from 0), or
|
|
310
|
+
two values (start, end). The axis is given in the work plane's own
|
|
311
|
+
coordinates by default: `pos` and `axis` with two values each, the
|
|
312
|
+
work plane's y axis if left out. Three values, or `axis='x'|'y'|'z'`,
|
|
313
|
+
give an axis in 3D coordinates instead (the model's x axis, not the
|
|
314
|
+
work plane's); a 3D axis without `pos` passes through the origin.
|
|
315
|
+
"""
|
|
316
|
+
if _comsol.parent_dim(geom) != 3 or _comsol.is_workplane(geom.java):
|
|
317
|
+
raise ValueError('Revolve needs a 3D geometry.')
|
|
318
|
+
if angle is not None:
|
|
319
|
+
if 'angle1' in properties or 'angle2' in properties:
|
|
320
|
+
raise ValueError('Give either angle or angle1/angle2.')
|
|
321
|
+
properties['angtype'] = 'specang'
|
|
322
|
+
if isinstance(angle, (list, tuple, numpy.ndarray)):
|
|
323
|
+
properties['angle1'], properties['angle2'] = angle
|
|
324
|
+
else:
|
|
325
|
+
properties['angle1'], properties['angle2'] = 0, angle
|
|
326
|
+
if isinstance(axis, str):
|
|
327
|
+
if axis not in ('x', 'y', 'z'):
|
|
328
|
+
raise ValueError(f"axis must be 'x', 'y', 'z' or a vector, not "
|
|
329
|
+
f'{axis!r}.')
|
|
330
|
+
axis = [int(a == axis) for a in 'xyz']
|
|
331
|
+
sizes = {len(v) for v in (pos, axis) if v is not None}
|
|
332
|
+
if len(sizes) > 1 or not sizes <= {2, 3}:
|
|
333
|
+
raise ValueError('pos and axis need two values each (work plane) or '
|
|
334
|
+
'three each (3D).')
|
|
335
|
+
if sizes == {3}:
|
|
336
|
+
if axis is None:
|
|
337
|
+
raise ValueError('A 3D pos needs an axis as well.')
|
|
338
|
+
properties.update(axistype='3d', axis3=axis,
|
|
339
|
+
pos3=pos if pos is not None else (0, 0, 0))
|
|
340
|
+
elif sizes == {2}:
|
|
341
|
+
properties.update(axistype='2d', pos=pos, axis=axis)
|
|
342
|
+
return feature(geom, 'Revolve', name=name, input=input, **properties)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def partition(parent: Node, /, input, tool, *, name: str = None,
|
|
346
|
+
**properties) -> Node:
|
|
347
|
+
"""
|
|
348
|
+
Creates a Partition of the `input` objects by `tool`.
|
|
349
|
+
|
|
350
|
+
`tool` is geometry objects, or a work plane (node, tag or name) to cut
|
|
351
|
+
along its plane. `keepinput`/`keeptool` keep the originals.
|
|
352
|
+
"""
|
|
353
|
+
plane = _workplane_tag(parent, tool)
|
|
354
|
+
if plane is not None:
|
|
355
|
+
properties.update(partitionwith='workplane', workplane=plane)
|
|
356
|
+
else:
|
|
357
|
+
properties['tool'] = tool
|
|
358
|
+
return feature(parent, 'Partition', name=name, input=input, **properties)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _workplane_tag(parent: Node, ref) -> str | None:
|
|
362
|
+
"""Returns the tag if `ref` (node, tag or name) is a work plane."""
|
|
363
|
+
if isinstance(ref, Node):
|
|
364
|
+
if not _comsol.is_workplane(ref.java):
|
|
365
|
+
return None
|
|
366
|
+
if _comsol.geometry_of(ref) != _comsol.geometry_of(parent):
|
|
367
|
+
raise ValueError(f'Work plane "{ref}" does not belong to '
|
|
368
|
+
f'"{parent}".')
|
|
369
|
+
return ref.tag()
|
|
370
|
+
if isinstance(ref, str):
|
|
371
|
+
container = _comsol.feature_container(parent)
|
|
372
|
+
try:
|
|
373
|
+
tag = _comsol.resolve_object(container, ref)
|
|
374
|
+
except LookupError:
|
|
375
|
+
return None
|
|
376
|
+
if (tag in [str(t) for t in container.tags()]
|
|
377
|
+
and _comsol.is_workplane(container.get(tag))):
|
|
378
|
+
return tag
|
|
379
|
+
return None
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def fillet(parent: Node, /, input, radius, *, name: str = None,
|
|
383
|
+
**properties) -> Node:
|
|
384
|
+
"""
|
|
385
|
+
Rounds edges (3D) or corners (2D and work planes) with `radius`.
|
|
386
|
+
|
|
387
|
+
`input` is geometry objects, meaning all their edges or corners, or
|
|
388
|
+
one selection of edges (3D) or points (2D) made with
|
|
389
|
+
`sel.*(..., where='geometry')` or `sel.cumulative()`. In a work plane,
|
|
390
|
+
pick single corners with a selection made in the plane, e.g.
|
|
391
|
+
`fillet(plane, sel.box(plane, 'point', x=1, y=1), 0.3)`. This is COMSOL's
|
|
392
|
+
Fillet3D `edge` in 3D and Fillet `point` in 2D. Array copies can be
|
|
393
|
+
named like `'arr1(1,1,1)'`; such names are only checked when the
|
|
394
|
+
geometry is built.
|
|
395
|
+
"""
|
|
396
|
+
return _round(parent, 'Fillet', input, name, radius=radius,
|
|
397
|
+
**properties)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def chamfer(parent: Node, /, input, dist, *, name: str = None,
|
|
401
|
+
**properties) -> Node:
|
|
402
|
+
"""
|
|
403
|
+
Bevels edges (3D) or corners (2D and work planes) by `dist`.
|
|
404
|
+
|
|
405
|
+
`input` works as in `fillet()`, including selections made in a work
|
|
406
|
+
plane for single corners. This is COMSOL's Chamfer3D `edge` with the
|
|
407
|
+
distance called `radius` in 3D, and Chamfer `point` with `dist` in 2D.
|
|
408
|
+
"""
|
|
409
|
+
if _comsol.parent_dim(parent) == 3:
|
|
410
|
+
return _round(parent, 'Chamfer', input, name, radius=dist,
|
|
411
|
+
**properties)
|
|
412
|
+
return _round(parent, 'Chamfer', input, name, dist=dist, **properties)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _round(parent: Node, kind: str, input, name, **properties) -> Node:
|
|
416
|
+
"""Creates a fillet or chamfer on edges (3D) or points (2D)."""
|
|
417
|
+
dim = _comsol.parent_dim(parent)
|
|
418
|
+
if dim == 3:
|
|
419
|
+
return feature(parent, f'{kind}3D', name=name, edge=input,
|
|
420
|
+
**properties)
|
|
421
|
+
if dim == 2:
|
|
422
|
+
return feature(parent, kind, name=name, point=input, **properties)
|
|
423
|
+
raise ValueError(f'A 1D geometry has no {kind.lower()}.')
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def line_segment(parent: Node, /, start, end, *, name: str = None,
|
|
427
|
+
**properties) -> Node:
|
|
428
|
+
"""Creates a straight line segment from `start` to `end`."""
|
|
429
|
+
_comsol.check_vector(parent, 'start', start)
|
|
430
|
+
_comsol.check_vector(parent, 'end', end)
|
|
431
|
+
return feature(parent, 'LineSegment', name=name, specify1='coord',
|
|
432
|
+
coord1=start, specify2='coord', coord2=end, **properties)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def interval(geom: Node, /, coord, *, name: str = None,
|
|
436
|
+
**properties) -> Node:
|
|
437
|
+
"""
|
|
438
|
+
Creates an Interval in a 1D geometry through the points `coord`.
|
|
439
|
+
|
|
440
|
+
Consecutive points bound one domain each, e.g. `[0, 1, 3]` gives two.
|
|
441
|
+
"""
|
|
442
|
+
if _comsol.parent_dim(geom) != 1:
|
|
443
|
+
raise ValueError('An interval needs a 1D geometry.')
|
|
444
|
+
if not isinstance(coord, (list, tuple, numpy.ndarray)) or len(coord) < 2:
|
|
445
|
+
raise ValueError(f'coord needs at least two points, not {coord!r}.')
|
|
446
|
+
return feature(geom, 'Interval', name=name, coord=coord, **properties)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
###############
|
|
450
|
+
# Work planes #
|
|
451
|
+
###############
|
|
452
|
+
|
|
453
|
+
def workplane(geom: Node, /, *, name: str = None, **properties) -> Node:
|
|
454
|
+
"""
|
|
455
|
+
Creates a Work Plane, for example `quickz=5`.
|
|
456
|
+
|
|
457
|
+
With `unite=True` the plane's 2D objects are imprinted into the 3D
|
|
458
|
+
geometry, e.g. to create an evaluation surface. Add 2D features with
|
|
459
|
+
`square(wp, ...)`, `rectangle(wp, ...)`, `circle(wp, ...)` or
|
|
460
|
+
`polygon(wp, ...)`.
|
|
461
|
+
"""
|
|
462
|
+
return feature(geom, 'WorkPlane', name=name, **properties)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def square(parent: Node, /, size=None, pos=None, *, name: str = None,
|
|
466
|
+
**properties) -> Node:
|
|
467
|
+
"""Creates a Square in a 2D geometry or a work plane."""
|
|
468
|
+
return feature(parent, 'Square', name=name, size=size, pos=pos,
|
|
469
|
+
**properties)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def rectangle(parent: Node, /, size=None, pos=None, *, name: str = None,
|
|
473
|
+
**properties) -> Node:
|
|
474
|
+
"""Creates a Rectangle `size=(width, height)` in 2D or a work plane."""
|
|
475
|
+
return feature(parent, 'Rectangle', name=name, size=size, pos=pos,
|
|
476
|
+
**properties)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def circle(parent: Node, /, r=None, pos=None, *, name: str = None,
|
|
480
|
+
**properties) -> Node:
|
|
481
|
+
"""Creates a Circle with radius `r` in 2D or a work plane."""
|
|
482
|
+
return feature(parent, 'Circle', name=name, r=r, pos=pos, **properties)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def polygon(parent: Node, /, x, y, *, name: str = None,
|
|
486
|
+
**properties) -> Node:
|
|
487
|
+
"""Creates a closed Polygon through the points `x`, `y`."""
|
|
488
|
+
properties.setdefault('type', 'solid')
|
|
489
|
+
return feature(parent, 'Polygon', name=name, source='vectors', x=x, y=y,
|
|
490
|
+
**properties)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def extrude(geom: Node, /, input, distance, *, name: str = None,
|
|
494
|
+
**properties) -> Node:
|
|
495
|
+
"""Extrudes `input` (e.g. a work plane) by `distance` (scalar or list)."""
|
|
496
|
+
if not isinstance(distance, (list, tuple)):
|
|
497
|
+
distance = [distance]
|
|
498
|
+
return feature(geom, 'Extrude', name=name, input=input,
|
|
499
|
+
distance=distance, **properties)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
##########
|
|
503
|
+
# Import #
|
|
504
|
+
##########
|
|
505
|
+
|
|
506
|
+
def import_(geom: Node, file, /, *, type: str = None, name: str = None,
|
|
507
|
+
**properties) -> Node:
|
|
508
|
+
"""
|
|
509
|
+
Imports geometry from `file`.
|
|
510
|
+
|
|
511
|
+
`type` defaults from the extension: `'native'` for `.mphbin`/`.mphtxt`,
|
|
512
|
+
otherwise `'cad'` (STEP, IGES, Parasolid, ...; needs a CAD-capable
|
|
513
|
+
license). The path is stored as an absolute path.
|
|
514
|
+
"""
|
|
515
|
+
from pathlib import Path
|
|
516
|
+
from .errors import LicenseError
|
|
517
|
+
file = Path(file).resolve()
|
|
518
|
+
if not file.exists():
|
|
519
|
+
raise FileNotFoundError(f'File "{file}" does not exist.')
|
|
520
|
+
if type is None:
|
|
521
|
+
type = 'native' if file.suffix.lower() in ('.mphbin', '.mphtxt') \
|
|
522
|
+
else 'cad'
|
|
523
|
+
node = feature(geom, 'Import', name=name, type=type, filename=str(file),
|
|
524
|
+
**properties)
|
|
525
|
+
try:
|
|
526
|
+
# MPh's `Node.import_()` calls `discardData()`, which geometry
|
|
527
|
+
# imports lack; `importData()` reads the file right away.
|
|
528
|
+
node.java.importData()
|
|
529
|
+
except Exception as error:
|
|
530
|
+
node.remove()
|
|
531
|
+
if 'license' in str(error).lower():
|
|
532
|
+
raise LicenseError(
|
|
533
|
+
f'Importing "{file.name}" needs a license for CAD import '
|
|
534
|
+
'(CAD Import Module, Design Module or a LiveLink).'
|
|
535
|
+
) from error
|
|
536
|
+
raise
|
|
537
|
+
return node
|
mphkit/py.typed
ADDED
|
File without changes
|
mphkit/sel.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geometry-based selections: select entities by location, not by number.
|
|
3
|
+
|
|
4
|
+
Every helper takes the geometry node first and returns an MPh node that
|
|
5
|
+
physics, materials, mesh and other features can `select()`. The queries
|
|
6
|
+
`entities` and `find` return entity numbers instead and leave nothing in
|
|
7
|
+
the model.
|
|
8
|
+
|
|
9
|
+
With `where='geometry'`, the level can also be `'object'`: whole geometry
|
|
10
|
+
objects, as COMSOL's "Object" level. Such a selection is only an input for
|
|
11
|
+
geometry operations (e.g. deleting or uniting the unnamed copies of an
|
|
12
|
+
array in a region), not something physics can use, so it is returned as
|
|
13
|
+
the selection feature in the geometry sequence rather than a
|
|
14
|
+
`selections/` node.
|
|
15
|
+
|
|
16
|
+
`where='component'` (the default for a geometry) creates a selection in
|
|
17
|
+
the component. It is evaluated on the finished geometry.
|
|
18
|
+
`where='geometry'` creates it inside the geometry sequence instead. Then
|
|
19
|
+
it can also be the input of later geometry operations, but it only sees
|
|
20
|
+
objects created before it.
|
|
21
|
+
|
|
22
|
+
A work plane can take the place of the geometry in `box`, `ball`, `disk`,
|
|
23
|
+
`all` and the set operations and `adjacent`, e.g. to fillet single
|
|
24
|
+
corners: `mk.fillet(plane, mk.sel.box(plane, 'point', x=1, y=1), 0.3)`.
|
|
25
|
+
Coordinates are the plane's own. Such selections live in the plane's
|
|
26
|
+
sequence (the default there), only see objects created before them, and
|
|
27
|
+
are inputs of operations in that plane only: COMSOL derives no
|
|
28
|
+
model-level selection from them, and physics refuses them ("Unknown
|
|
29
|
+
selection"). They are returned as the selection feature in the plane.
|
|
30
|
+
"""
|
|
31
|
+
from ._sel import adjacent, all_ as all, ball, box, complement, \
|
|
32
|
+
cumulative, cylinder, difference, disk, entities, find, intersection, \
|
|
33
|
+
layer, result, union
|
|
34
|
+
|
|
35
|
+
__all__ = ['adjacent', 'all', 'ball', 'box', 'complement', 'cumulative',
|
|
36
|
+
'cylinder', 'difference', 'disk', 'entities', 'find',
|
|
37
|
+
'intersection', 'layer', 'result', 'union']
|