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/_sel.py
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
"""Geometry-based selections; documented in `mphkit.sel`."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from mph import Node
|
|
5
|
+
from mph.node import escape
|
|
6
|
+
|
|
7
|
+
from . import _comsol
|
|
8
|
+
from .geometry import feature
|
|
9
|
+
|
|
10
|
+
WHERE = ('component', 'geometry')
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _where(parent: Node, where: str | None) -> str:
|
|
14
|
+
"""
|
|
15
|
+
Resolves `where`: by default the component for a geometry and the
|
|
16
|
+
plane's own sequence for a work plane, which has no component
|
|
17
|
+
selections.
|
|
18
|
+
"""
|
|
19
|
+
workplane = _comsol.is_workplane(parent.java)
|
|
20
|
+
if where is None:
|
|
21
|
+
return 'geometry' if workplane else 'component'
|
|
22
|
+
if where not in WHERE:
|
|
23
|
+
raise ValueError(f'where must be one of {WHERE}, not {where!r}.')
|
|
24
|
+
if workplane and where == 'component':
|
|
25
|
+
raise ValueError('A work plane has no component selections; its '
|
|
26
|
+
'selections are made in the plane (leave out '
|
|
27
|
+
'where).')
|
|
28
|
+
return where
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _level(geom: Node, entity: str, where: str | None) -> int:
|
|
32
|
+
"""Maps an entity name to `entitydim`; `'object'` is -1 (geometry only)."""
|
|
33
|
+
where = _where(geom, where)
|
|
34
|
+
if entity == 'object':
|
|
35
|
+
if where != 'geometry':
|
|
36
|
+
raise ValueError("The 'object' level is for inputs of geometry "
|
|
37
|
+
"operations; use it with where='geometry'.")
|
|
38
|
+
return -1
|
|
39
|
+
return _comsol.entity_dim(geom, entity)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _create(geom: Node, type: str, where: str, name: str | None,
|
|
43
|
+
properties: dict, default: str = None) -> Node:
|
|
44
|
+
"""Creates a selection of `type` (component names, e.g. 'Box')."""
|
|
45
|
+
where = _where(geom, where)
|
|
46
|
+
model = geom.model
|
|
47
|
+
if _comsol.is_workplane(geom.java):
|
|
48
|
+
# A work plane derives no model-level selections: the feature in
|
|
49
|
+
# the plane is the input for later operations there.
|
|
50
|
+
return feature(geom, f'{type}Selection', name=name, **properties)
|
|
51
|
+
if where == 'geometry':
|
|
52
|
+
node = feature(geom, f'{type}Selection', name=name, **properties)
|
|
53
|
+
if properties.get('entitydim') == -1:
|
|
54
|
+
# Objects: COMSOL derives four same-labeled selections, none of
|
|
55
|
+
# them for physics. The feature itself is the input to use.
|
|
56
|
+
return node
|
|
57
|
+
derived = model/'selections'/escape(node.name())
|
|
58
|
+
_comsol.check_tag(derived, f'{geom.tag()}_{node.tag()}')
|
|
59
|
+
return derived
|
|
60
|
+
container = _comsol.component_of(geom).selection()
|
|
61
|
+
taken = _comsol.selection_labels(model)
|
|
62
|
+
tag, label = _comsol.create_java(container, 'selections', type, name,
|
|
63
|
+
taken, tags=model.java.selection(),
|
|
64
|
+
default=default)
|
|
65
|
+
node = model/'selections'/escape(label)
|
|
66
|
+
try:
|
|
67
|
+
_comsol.check_tag(node, tag)
|
|
68
|
+
java = container.get(tag)
|
|
69
|
+
_comsol.set_properties(java, properties)
|
|
70
|
+
except Exception:
|
|
71
|
+
container.remove(tag)
|
|
72
|
+
raise
|
|
73
|
+
return node
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _inputs(geom: Node, where: str, values) -> list[str]:
|
|
77
|
+
"""Returns selection tags for the inputs of a set operation."""
|
|
78
|
+
where = _where(geom, where)
|
|
79
|
+
values = [values] if isinstance(values, (Node, str)) else list(values)
|
|
80
|
+
if where == 'component':
|
|
81
|
+
for value in values:
|
|
82
|
+
if isinstance(value, Node) and value.path[0] == 'geometries':
|
|
83
|
+
raise TypeError(f'"{value}" is a selection in the geometry '
|
|
84
|
+
"sequence; use where='geometry' with it.")
|
|
85
|
+
return _comsol.names(values)
|
|
86
|
+
tags = []
|
|
87
|
+
for value in values:
|
|
88
|
+
if isinstance(value, Node):
|
|
89
|
+
source = _comsol.selection_source(geom, value)
|
|
90
|
+
if source is None:
|
|
91
|
+
raise TypeError(f'"{value}" is not a selection.')
|
|
92
|
+
if source[2] == 'cumulative':
|
|
93
|
+
raise TypeError(
|
|
94
|
+
f'Selection "{value}" is a cumulative selection; set '
|
|
95
|
+
"operations with where='geometry' take selections made "
|
|
96
|
+
"with where='geometry'.")
|
|
97
|
+
tags.append(source[0])
|
|
98
|
+
else:
|
|
99
|
+
tags.append(str(value))
|
|
100
|
+
return tags
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _bounds(geom: Node, **ranges) -> dict:
|
|
104
|
+
"""Turns x/y/z ranges into Box properties; a scalar is a plane."""
|
|
105
|
+
dim = _comsol.parent_dim(geom)
|
|
106
|
+
properties = {}
|
|
107
|
+
for axis, value in ranges.items():
|
|
108
|
+
if value is None:
|
|
109
|
+
continue
|
|
110
|
+
if 'xyz'.index(axis) >= dim:
|
|
111
|
+
raise ValueError(f'A {dim}D geometry has no {axis} coordinate.')
|
|
112
|
+
if isinstance(value, (list, tuple)):
|
|
113
|
+
low, high = value
|
|
114
|
+
else:
|
|
115
|
+
low = high = value
|
|
116
|
+
properties[f'{axis}min'] = low
|
|
117
|
+
properties[f'{axis}max'] = high
|
|
118
|
+
return properties
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def box(geom: Node, entity: str, /, x=None, y=None, z=None, *,
|
|
122
|
+
condition: str = 'inside', where: str | None = None,
|
|
123
|
+
name: str = None, **properties) -> Node:
|
|
124
|
+
"""
|
|
125
|
+
Selects the entities inside a box.
|
|
126
|
+
|
|
127
|
+
`x`, `y`, `z` are `(min, max)` ranges; a single value such as `z=0`
|
|
128
|
+
selects on that plane. Omitted coordinates are unbounded. With
|
|
129
|
+
`where='geometry'` (or in a work plane), `entity` may be `'object'` to
|
|
130
|
+
select whole objects as input of a geometry operation. `geom` may also
|
|
131
|
+
be a work plane, in the plane's coordinates, e.g. to fillet one corner
|
|
132
|
+
there (see `mphkit.sel` for what such selections can be used for).
|
|
133
|
+
|
|
134
|
+
`condition` defaults to `'inside'` (entity entirely inside the box).
|
|
135
|
+
This differs from COMSOL's default `'intersects'`, which on a plane
|
|
136
|
+
would also pick every face touching it. Other values: `'intersects'`,
|
|
137
|
+
`'allvertices'`, `'somevertex'`.
|
|
138
|
+
"""
|
|
139
|
+
properties = {'entitydim': _level(geom, entity, where),
|
|
140
|
+
'condition': condition,
|
|
141
|
+
**_bounds(geom, x=x, y=y, z=z), **properties}
|
|
142
|
+
return _create(geom, 'Box', where, name, properties)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def ball(geom: Node, entity: str, /, center, r, *,
|
|
146
|
+
condition: str = 'inside', where: str | None = None,
|
|
147
|
+
name: str = None, **properties) -> Node:
|
|
148
|
+
"""
|
|
149
|
+
Selects the entities inside a ball of radius `r` around `center`.
|
|
150
|
+
|
|
151
|
+
`condition` defaults to `'inside'`, see `box()`.
|
|
152
|
+
"""
|
|
153
|
+
dim = _comsol.parent_dim(geom)
|
|
154
|
+
if len(center) != dim:
|
|
155
|
+
raise ValueError(f'center needs {dim} coordinates.')
|
|
156
|
+
position = {f'pos{a}': c for a, c in zip('xyz', center)}
|
|
157
|
+
properties = {'entitydim': _level(geom, entity, where),
|
|
158
|
+
'condition': condition, 'r': r, **position, **properties}
|
|
159
|
+
return _create(geom, 'Ball', where, name, properties)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def cylinder(geom: Node, entity: str, /, pos, r, *, axis=None, top=None,
|
|
163
|
+
bottom=None, rin=None, condition: str = 'inside',
|
|
164
|
+
where: str | None = None, name: str = None,
|
|
165
|
+
**properties) -> Node:
|
|
166
|
+
"""
|
|
167
|
+
Selects the entities inside a cylinder (3D).
|
|
168
|
+
|
|
169
|
+
`pos` is the center of the base and `axis` the direction, `'x'`, `'y'`,
|
|
170
|
+
`'z'` (default) or a vector. `top` and `bottom` are measured from `pos`
|
|
171
|
+
along the axis; left out, the cylinder is unbounded. With `rin` it is a
|
|
172
|
+
shell, e.g. `r=2.1, rin=1.9` picks the side faces of an r=2 cylinder.
|
|
173
|
+
`condition` defaults to `'inside'`, see `box()`. In 2D use `disk()`.
|
|
174
|
+
"""
|
|
175
|
+
_comsol.check_not_workplane(geom, 'sel.cylinder')
|
|
176
|
+
if _comsol.sdim(geom) != 3:
|
|
177
|
+
raise ValueError('sel.cylinder needs a 3D geometry; use sel.disk in '
|
|
178
|
+
'2D.')
|
|
179
|
+
_comsol.check_vector(geom, 'pos', pos)
|
|
180
|
+
if axis is not None and not isinstance(axis, str):
|
|
181
|
+
_comsol.check_vector(geom, 'axis', axis)
|
|
182
|
+
properties = {'entitydim': _level(geom, entity, where),
|
|
183
|
+
'condition': condition, 'pos': pos, 'r': r, 'rin': rin,
|
|
184
|
+
'top': top, 'bottom': bottom,
|
|
185
|
+
**_comsol.axis_properties(axis), **properties}
|
|
186
|
+
return _create(geom, 'Cylinder', where, name, properties)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def disk(geom: Node, entity: str, /, center, r, *, rin=None,
|
|
190
|
+
condition: str = 'inside', where: str | None = None,
|
|
191
|
+
name: str = None, **properties) -> Node:
|
|
192
|
+
"""
|
|
193
|
+
Selects the entities inside a disk of radius `r` around `center` (2D).
|
|
194
|
+
|
|
195
|
+
With `rin` it is a ring. `condition` defaults to `'inside'`, see
|
|
196
|
+
`box()`. In 3D use `cylinder()` or `ball()`.
|
|
197
|
+
"""
|
|
198
|
+
if _comsol.parent_dim(geom) != 2:
|
|
199
|
+
raise ValueError('sel.disk needs a 2D geometry; use sel.cylinder or '
|
|
200
|
+
'sel.ball in 3D.')
|
|
201
|
+
_comsol.check_vector(geom, 'center', center)
|
|
202
|
+
properties = {'entitydim': _level(geom, entity, where),
|
|
203
|
+
'condition': condition, 'posx': center[0],
|
|
204
|
+
'posy': center[1], 'r': r, 'rin': rin, **properties}
|
|
205
|
+
return _create(geom, 'Disk', where, name, properties)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def all_(geom: Node, entity: str, /, *, where: str | None = None,
|
|
209
|
+
name: str = None) -> Node:
|
|
210
|
+
"""Selects all entities of one kind, e.g. all boundaries."""
|
|
211
|
+
properties = {'entitydim': _level(geom, entity, where),
|
|
212
|
+
'condition': 'intersects'}
|
|
213
|
+
return _create(geom, 'Box', where, name, properties)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def union(geom: Node, entity: str, /, input, *, where: str | None = None,
|
|
217
|
+
name: str = None) -> Node:
|
|
218
|
+
"""Selects the union of the `input` selections."""
|
|
219
|
+
properties = {'entitydim': _level(geom, entity, where),
|
|
220
|
+
'input': _inputs(geom, where, input)}
|
|
221
|
+
return _create(geom, 'Union', where, name, properties)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def intersection(geom: Node, entity: str, /, input, *,
|
|
225
|
+
where: str | None = None, name: str = None) -> Node:
|
|
226
|
+
"""Selects the entities that all `input` selections have in common."""
|
|
227
|
+
properties = {'entitydim': _level(geom, entity, where),
|
|
228
|
+
'input': _inputs(geom, where, input)}
|
|
229
|
+
return _create(geom, 'Intersection', where, name, properties)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def difference(geom: Node, entity: str, /, add, subtract, *,
|
|
233
|
+
where: str | None = None, name: str = None) -> Node:
|
|
234
|
+
"""Selects the entities in `add` that are not in `subtract`."""
|
|
235
|
+
properties = {'entitydim': _level(geom, entity, where),
|
|
236
|
+
'add': _inputs(geom, where, add),
|
|
237
|
+
'subtract': _inputs(geom, where, subtract)}
|
|
238
|
+
return _create(geom, 'Difference', where, name, properties)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def complement(geom: Node, entity: str, /, input, *,
|
|
242
|
+
where: str | None = None, name: str = None) -> Node:
|
|
243
|
+
"""Selects all entities that are not in the `input` selections."""
|
|
244
|
+
properties = {'entitydim': _level(geom, entity, where),
|
|
245
|
+
'input': _inputs(geom, where, input)}
|
|
246
|
+
return _create(geom, 'Complement', where, name, properties)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def adjacent(geom: Node, /, input, entity: str = 'boundary', *,
|
|
250
|
+
input_entity: str = 'domain', exterior: bool = True,
|
|
251
|
+
interior: bool = False, where: str | None = None,
|
|
252
|
+
name: str = None) -> Node:
|
|
253
|
+
"""
|
|
254
|
+
Selects the entities of kind `entity` adjacent to the `input` selections.
|
|
255
|
+
|
|
256
|
+
For example the exterior boundaries of a domain selection. The inputs
|
|
257
|
+
are of kind `input_entity`.
|
|
258
|
+
"""
|
|
259
|
+
properties = {'entitydim': _comsol.entity_dim(geom, input_entity),
|
|
260
|
+
'outputdim': _comsol.entity_dim(geom, entity),
|
|
261
|
+
'input': _inputs(geom, where, input),
|
|
262
|
+
'exterior': exterior, 'interior': interior}
|
|
263
|
+
return _create(geom, 'Adjacent', where, name, properties)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def result(geom: Node, feature: Node, entity: str, /, *,
|
|
267
|
+
name: str = None) -> Node:
|
|
268
|
+
"""
|
|
269
|
+
Selects the entities created by a geometry feature.
|
|
270
|
+
|
|
271
|
+
Turns on the feature's result selection (`selresult`) and returns a
|
|
272
|
+
named component selection that physics can use, without relying on
|
|
273
|
+
COMSOL's tag convention. Calling it again returns the same selection.
|
|
274
|
+
Because it switches on `selresult`, a Java export of the model shows
|
|
275
|
+
that setting on the feature; the geometry is not affected.
|
|
276
|
+
"""
|
|
277
|
+
if len(feature.path) != 3 or _comsol.geometry_of(feature) != geom:
|
|
278
|
+
raise ValueError(f'"{feature}" is not a top-level feature of '
|
|
279
|
+
f'"{geom}".')
|
|
280
|
+
entity = _comsol.entity_name(geom, entity)
|
|
281
|
+
suffix = _comsol.RESULT_SUFFIX[entity]
|
|
282
|
+
java = feature.java
|
|
283
|
+
known = [str(p) for p in java.properties()]
|
|
284
|
+
if 'selresult' not in known:
|
|
285
|
+
raise ValueError(f'"{feature}" has no result selection.')
|
|
286
|
+
own = f'{geom.tag()}_{feature.tag()}'
|
|
287
|
+
clash = _comsol.selection_labels(geom.model, exclude_prefix=own)
|
|
288
|
+
if feature.name() in clash:
|
|
289
|
+
raise ValueError(f'A selection is already named "{feature.name()}"; '
|
|
290
|
+
'rename the feature first.')
|
|
291
|
+
was_on = str(java.getString('selresult')) == 'on'
|
|
292
|
+
_comsol.set_property(java, 'selresult', True)
|
|
293
|
+
if 'selresultshow' in known:
|
|
294
|
+
show = str(java.getString('selresultshow'))
|
|
295
|
+
if not was_on:
|
|
296
|
+
java.set('selresultshow', suffix)
|
|
297
|
+
elif show not in (suffix, 'all'):
|
|
298
|
+
java.set('selresultshow', 'all')
|
|
299
|
+
derived = _comsol.result_tag(geom, feature, entity)
|
|
300
|
+
return _wrap(geom, derived, entity, name,
|
|
301
|
+
default=f'{feature.name()} ({entity})')
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def layer(geom: Node, feature: Node, layer, /, *, name: str = None) -> Node:
|
|
305
|
+
"""
|
|
306
|
+
Selects the domains of one layer of a geometry feature.
|
|
307
|
+
|
|
308
|
+
Layers are the shells a Block, Cylinder, Sphere or Rectangle can be
|
|
309
|
+
given (`layername`, `layer`, `layertop`, ...), typically a perfectly
|
|
310
|
+
matched layer. `layer` is a name from `layername`, its number counted
|
|
311
|
+
from 1, or `'core'` for the part outside every layer.
|
|
312
|
+
|
|
313
|
+
COMSOL derives these selections for domains only, and they survive
|
|
314
|
+
boolean operations on the feature. They are empty when the feature is
|
|
315
|
+
consumed, for instance as the subtracted object of a difference; a
|
|
316
|
+
`sel.box` over the same region is the alternative then. Calling this
|
|
317
|
+
again returns the same selection. Like `result()`, it switches a
|
|
318
|
+
setting on the feature (`sellayer`), which shows in a Java export.
|
|
319
|
+
"""
|
|
320
|
+
if len(feature.path) != 3 or _comsol.geometry_of(feature) != geom:
|
|
321
|
+
raise ValueError(f'"{feature}" is not a top-level feature of '
|
|
322
|
+
f'"{geom}".')
|
|
323
|
+
java = feature.java
|
|
324
|
+
known = [str(p) for p in java.properties()]
|
|
325
|
+
if 'sellayer' not in known:
|
|
326
|
+
raise ValueError(f'"{feature}" does not support layers.')
|
|
327
|
+
names = [str(n) for n in java.getStringArray('layername')] \
|
|
328
|
+
if 'layername' in known else []
|
|
329
|
+
if isinstance(layer, str) and layer.lower() == 'core':
|
|
330
|
+
index, expected = None, 'Core'
|
|
331
|
+
elif isinstance(layer, bool) or not isinstance(layer, (int, str)):
|
|
332
|
+
raise TypeError(f'A layer is a name, a number or "core", '
|
|
333
|
+
f'not {layer!r}.')
|
|
334
|
+
elif isinstance(layer, int):
|
|
335
|
+
index = layer
|
|
336
|
+
expected = names[index - 1] if 0 < index <= len(names) else None
|
|
337
|
+
elif layer in names:
|
|
338
|
+
index = names.index(layer) + 1
|
|
339
|
+
expected = layer
|
|
340
|
+
else:
|
|
341
|
+
raise ValueError(f'"{feature}" has no layer named "{layer}". '
|
|
342
|
+
f'Known names: {names or "none"} (or a number, '
|
|
343
|
+
f'or "core").')
|
|
344
|
+
_comsol.set_property(java, 'sellayer', True)
|
|
345
|
+
derived = _comsol.layer_tag(geom, feature, index)
|
|
346
|
+
selections = geom.model.java.selection()
|
|
347
|
+
if derived not in [str(t) for t in selections.tags()]:
|
|
348
|
+
prefix = f'{geom.tag()}_{feature.tag()}_'
|
|
349
|
+
found = [str(t) for t in selections.tags()
|
|
350
|
+
if str(t).startswith(prefix) and 'layer' in str(t)
|
|
351
|
+
or str(t) == f'{prefix}core']
|
|
352
|
+
raise ValueError(f'"{feature}" has no layer selection "{derived}". '
|
|
353
|
+
f'Found: {found or "none"}.')
|
|
354
|
+
label = str(selections.get(derived).label())
|
|
355
|
+
if expected and not label.startswith(expected):
|
|
356
|
+
raise RuntimeError(f'Selection "{derived}" is labeled "{label}", '
|
|
357
|
+
f'expected it to start with "{expected}". The '
|
|
358
|
+
'COMSOL naming convention may have changed.')
|
|
359
|
+
return _wrap(geom, derived, 'domain', name,
|
|
360
|
+
default=f'{feature.name()} ({expected or index})')
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def entities(geom: Node, selection: Node, /) -> list[int]:
|
|
364
|
+
"""
|
|
365
|
+
Returns the entity numbers of a selection, in ascending order.
|
|
366
|
+
|
|
367
|
+
The numbers are at the selection's own level: an adjacent selection of
|
|
368
|
+
domains gives boundary numbers. Entity numbers change when the geometry
|
|
369
|
+
changes, so use them to check or inspect a model, not to set up physics.
|
|
370
|
+
The geometry must be built; a selection made with `where='geometry'`
|
|
371
|
+
changes the geometry sequence, so build again after creating one.
|
|
372
|
+
"""
|
|
373
|
+
_comsol.check_not_workplane(geom, 'sel.entities')
|
|
374
|
+
_comsol.check_built(geom)
|
|
375
|
+
java = _comsol.check_selection(geom, selection)
|
|
376
|
+
return sorted(int(e) for e in java.entities())
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def find(geom: Node, entity: str, /, x=None, y=None, z=None, *,
|
|
380
|
+
condition: str = 'inside') -> list[int]:
|
|
381
|
+
"""
|
|
382
|
+
Returns the numbers of the entities inside a box, without a selection.
|
|
383
|
+
|
|
384
|
+
Takes the same arguments as `box()`, but removes the selection again
|
|
385
|
+
and returns the entity numbers. Meant for checks and lookups: entity
|
|
386
|
+
numbers change with the geometry, so physics should use `box()`. For a
|
|
387
|
+
lookup of another kind, create the selection, read it with `entities()`
|
|
388
|
+
and `remove()` it; leave out `name` to avoid label clashes.
|
|
389
|
+
"""
|
|
390
|
+
_comsol.check_not_workplane(geom, 'sel.find')
|
|
391
|
+
node = box(geom, entity, x, y, z, condition=condition)
|
|
392
|
+
try:
|
|
393
|
+
return entities(geom, node)
|
|
394
|
+
finally:
|
|
395
|
+
_comsol.component_of(geom).selection().remove(node.tag())
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def cumulative(geom: Node, group, entity: str, /, *, create: bool = False,
|
|
399
|
+
name: str = None) -> Node:
|
|
400
|
+
"""
|
|
401
|
+
Selects the entities of a cumulative selection, COMSOL's way of
|
|
402
|
+
collecting what several features create.
|
|
403
|
+
|
|
404
|
+
In the COMSOL Desktop a feature's "Contribute to" list has a New
|
|
405
|
+
button that creates a cumulative selection; other features pick it
|
|
406
|
+
from the list, and physics offers it per level, e.g.
|
|
407
|
+
"holes (Boundary)". The same steps here:
|
|
408
|
+
|
|
409
|
+
```python
|
|
410
|
+
holes = mk.sel.cumulative(geom, 'holes', 'domain', create=True) # New
|
|
411
|
+
mk.cylinder(geom, 1, 5, (x, 0, 0), contributeto=holes) # Contribute to
|
|
412
|
+
walls = mk.sel.cumulative(geom, 'holes', 'boundary') # holes (Boundary)
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
`group` is the cumulative selection's name, or a node returned by this
|
|
416
|
+
function. `create=True` creates it and fails if it exists; otherwise it
|
|
417
|
+
must exist, so a typo raises instead of giving an empty selection.
|
|
418
|
+
Features take the node or the name as `contributeto`. It holds every
|
|
419
|
+
entity of the contributing objects and survives later Boolean
|
|
420
|
+
operations. Only top-level features of the geometry can contribute,
|
|
421
|
+
not features inside a work plane. Calling this again returns the same
|
|
422
|
+
selection.
|
|
423
|
+
"""
|
|
424
|
+
_comsol.check_not_workplane(geom, 'sel.cumulative')
|
|
425
|
+
entity = _comsol.entity_name(geom, entity)
|
|
426
|
+
model = geom.model
|
|
427
|
+
selections = geom.java.selection()
|
|
428
|
+
known = _comsol.cumulative_tags(geom)
|
|
429
|
+
if create:
|
|
430
|
+
if not isinstance(group, str):
|
|
431
|
+
raise TypeError('create=True takes the name of a new cumulative '
|
|
432
|
+
f'selection, not {group!r}.')
|
|
433
|
+
if group == 'none':
|
|
434
|
+
raise ValueError('"none" means "no cumulative selection" in '
|
|
435
|
+
'COMSOL; choose another name.')
|
|
436
|
+
if group in known:
|
|
437
|
+
raise ValueError(f'Cumulative selection "{group}" already exists '
|
|
438
|
+
f'in "{geom}".')
|
|
439
|
+
taken = _comsol.selection_labels(model)
|
|
440
|
+
if group in taken:
|
|
441
|
+
raise ValueError(f'A selection is already named "{group}".')
|
|
442
|
+
if name is not None:
|
|
443
|
+
_comsol.pick_label(name, name, taken)
|
|
444
|
+
tag = str(selections.uniquetag('csel'))
|
|
445
|
+
selections.create(tag, 'CumulativeSelection')
|
|
446
|
+
selections.get(tag).label(group)
|
|
447
|
+
else:
|
|
448
|
+
tag = _comsol.cumulative_tag(geom, group)
|
|
449
|
+
try:
|
|
450
|
+
derived = f'{geom.tag()}_{tag}_{_comsol.RESULT_SUFFIX[entity]}'
|
|
451
|
+
tags = [str(t) for t in model.java.selection().tags()]
|
|
452
|
+
if derived not in tags:
|
|
453
|
+
prefix = f'{geom.tag()}_{tag}_'
|
|
454
|
+
found = [t for t in tags if t.startswith(prefix)]
|
|
455
|
+
raise ValueError(f'Cumulative selection "{group}" has no '
|
|
456
|
+
f'selection "{derived}". Found: {found}.')
|
|
457
|
+
label = str(selections.get(tag).label())
|
|
458
|
+
return _wrap(geom, derived, entity, name,
|
|
459
|
+
default=f'{label} ({entity})')
|
|
460
|
+
except Exception:
|
|
461
|
+
if create:
|
|
462
|
+
selections.remove(tag)
|
|
463
|
+
raise
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _wrap(geom: Node, derived: str, entity: str, name: str | None,
|
|
467
|
+
default: str) -> Node:
|
|
468
|
+
"""
|
|
469
|
+
Returns a component selection that stands for a derived selection.
|
|
470
|
+
|
|
471
|
+
MPh lists selections COMSOL derives from a geometry feature under that
|
|
472
|
+
feature's label, so several can share one name and none of them can be
|
|
473
|
+
addressed by path. A Union with a unique label and the derived tag as
|
|
474
|
+
its input can. An existing wrapper is found by its input, so renaming
|
|
475
|
+
the feature does not create a second one.
|
|
476
|
+
"""
|
|
477
|
+
container = _comsol.component_of(geom).selection()
|
|
478
|
+
for tag in container.tags():
|
|
479
|
+
member = container.get(tag)
|
|
480
|
+
if (str(member.getType()) == 'Union'
|
|
481
|
+
and [str(t) for t in member.getStringArray('input')]
|
|
482
|
+
== [derived]):
|
|
483
|
+
return geom.model/'selections'/escape(member.label())
|
|
484
|
+
properties = {'entitydim': _comsol.entity_dim(geom, entity),
|
|
485
|
+
'input': [derived]}
|
|
486
|
+
return _create(geom, 'Union', 'component', name, properties,
|
|
487
|
+
default=default)
|