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/_expr.py ADDED
@@ -0,0 +1,37 @@
1
+ """Conversion of Python values to COMSOL expression strings."""
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import Sequence
5
+ from numbers import Real
6
+
7
+ Expr = str | Real
8
+ Vector = Sequence[Expr]
9
+
10
+
11
+ def expr(value: Expr) -> str:
12
+ """Return `value` as a COMSOL expression string.
13
+
14
+ Numbers are written in plain form (interpreted in the geometry's length
15
+ unit); strings are passed through, so `'5[mm]'` or `'r0/2'` work as-is.
16
+ """
17
+ if isinstance(value, bool):
18
+ raise TypeError(f'Expected a number or expression, got {value!r}.')
19
+ if isinstance(value, str):
20
+ return value
21
+ if isinstance(value, Real):
22
+ return repr(float(value)) if not float(value).is_integer() else str(int(value))
23
+ raise TypeError(f'Expected a number or expression, got {value!r}.')
24
+
25
+
26
+ def vector(values: Vector, length: int | None = None) -> list[str]:
27
+ """Return a sequence of numbers/expressions as a list of strings.
28
+
29
+ MPh casts a list based on its first item, so a mixed list such as
30
+ `[0, 'x1', 0]` fails there. Converting every item to a string avoids that.
31
+ """
32
+ if isinstance(values, str):
33
+ raise TypeError(f'Expected a sequence, got the string {values!r}.')
34
+ items = [expr(v) for v in values]
35
+ if length is not None and len(items) != length:
36
+ raise ValueError(f'Expected {length} components, got {len(items)}.')
37
+ return items
mphkit/_measure.py ADDED
@@ -0,0 +1,102 @@
1
+ """
2
+ Measurements of the finished geometry. Public as `mk.measure` and
3
+ `mk.bounding_box`.
4
+
5
+ Both return plain numbers in the geometry's length unit and leave nothing
6
+ in the model. Curved geometry is measured on a rendering mesh, so volumes,
7
+ areas and lengths of curved entities are approximate (a cylinder about
8
+ 0.3 % too small, a sphere 0.3 to 0.6 % depending on the geometry kernel);
9
+ planar geometry is exact.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import numbers
14
+
15
+ import numpy
16
+ from mph import Node
17
+
18
+ from . import _comsol
19
+
20
+ AXES = 'xyz'
21
+
22
+
23
+ def _final(geom: Node, entity: str, selection):
24
+ """
25
+ Returns a measurement of the finished geometry, or `None` if empty.
26
+
27
+ `selection` is an entity number, a list of numbers, a selection node at
28
+ the level of `entity`, or `None` for all entities of that level.
29
+ """
30
+ _comsol.check_built(geom)
31
+ dim = _comsol.entity_dim(geom, entity)
32
+ count = _comsol.entity_count(geom, dim)
33
+ if selection is None:
34
+ found = list(range(1, count + 1))
35
+ elif isinstance(selection, Node):
36
+ java = _comsol.check_selection(geom, selection)
37
+ level = [int(d) for d in java.dimension()]
38
+ if level != [dim]:
39
+ raise ValueError(f'Selection "{selection}" is not a {entity} '
40
+ 'selection.')
41
+ found = [int(e) for e in java.entities()]
42
+ else:
43
+ items = selection if isinstance(
44
+ selection, (list, tuple, numpy.ndarray)) else [selection]
45
+ found = []
46
+ for item in items:
47
+ if isinstance(item, bool) or not isinstance(item, numbers.Integral):
48
+ raise TypeError(f'Expected entity numbers, a selection node '
49
+ f'or None, not {selection!r}.')
50
+ found.append(int(item))
51
+ wrong = [n for n in found if not 1 <= n <= count]
52
+ if wrong:
53
+ raise ValueError(f'No {entity} {wrong} in geometry "{geom}"; it '
54
+ f'has {count} {entity} entities.')
55
+ if not found:
56
+ return None
57
+ measurement = geom.java.measureFinal()
58
+ measurement.selection().geom(geom.tag(), dim)
59
+ measurement.selection().set(found)
60
+ return measurement
61
+
62
+
63
+ def measure(geom: Node, entity: str, /, selection=None) -> float:
64
+ """
65
+ Returns the size of entities: volume, area or length.
66
+
67
+ What is measured follows the level of `entity`: the volume of domains
68
+ (the area of 2D domains), the area of boundaries, the length of edges.
69
+ `selection` is an entity number, a list of them, a selection node, or
70
+ `None` for all entities of that kind. Several entities give the sum; a
71
+ number given twice counts once. An empty selection gives 0.
72
+
73
+ The geometry must be built. Curved entities are measured on a
74
+ rendering mesh and are approximate (a cylinder about 0.3 % too small);
75
+ planar ones are exact.
76
+ """
77
+ _comsol.check_not_workplane(geom, 'measure')
78
+ if _comsol.entity_dim(geom, entity) == 0:
79
+ raise ValueError('Points have no size; use bounding_box() for their '
80
+ 'coordinates.')
81
+ measurement = _final(geom, entity, selection)
82
+ return 0.0 if measurement is None else float(measurement.getVolume())
83
+
84
+
85
+ def bounding_box(geom: Node, entity: str, /, selection=None) -> dict | None:
86
+ """
87
+ Returns the bounding box of entities as `{'x': (min, max), ...}`.
88
+
89
+ Has one pair per space dimension, so it can be passed on as
90
+ `sel.box(geom, entity, **box)`. Allow a margin then: the values may
91
+ carry single-precision noise, which makes `condition='inside'` miss
92
+ the entity itself. For a single point, min equals max: its
93
+ coordinates. `selection` works as in `measure()`. Returns `None` for
94
+ an empty selection.
95
+ """
96
+ _comsol.check_not_workplane(geom, 'bounding_box')
97
+ measurement = _final(geom, entity, selection)
98
+ if measurement is None:
99
+ return None
100
+ values = [float(v) for v in measurement.getBoundingBox()]
101
+ return {axis: (values[2*i], values[2*i + 1])
102
+ for i, axis in enumerate(AXES[:len(values) // 2])}
mphkit/_props.py ADDED
@@ -0,0 +1,62 @@
1
+ """
2
+ Setting properties of any COMSOL object. Public as `mk.set`.
3
+
4
+ Calling a Java `set()` directly fails for plain Python ints ("Ambiguous
5
+ overloads"), numpy arrays and lists that mix numbers and expressions;
6
+ `mk.set` converts them the same way the other helpers do.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from mph import Node
11
+ from mph.node import join
12
+
13
+ from . import _comsol
14
+ from ._comsol import WorkPlaneNode
15
+
16
+
17
+ def set_(target, /, **properties):
18
+ """
19
+ Sets properties of an MPh node or a Java object, and returns it.
20
+
21
+ For example on a mesh size feature,
22
+ `mk.set(size, custom=True, hmax='L/10', hgrad=1.45)`. The target may be
23
+ any MPh node, or a Java object MPh does not reach, such as a probe, a
24
+ material function or `physics.java.prop('ShapeProperty')`. Keyword
25
+ arguments are COMSOL property names, set in the given order (e.g.
26
+ `custom` before `hmax`); `None` values are skipped.
27
+
28
+ Ints, numpy arrays and lists mixing numbers and expressions are
29
+ converted for COMSOL; lists of numbers become string arrays. Unknown
30
+ names raise `ValueError` with a suggestion, invalid choices list the
31
+ allowed values. If one property fails, the ones before it stay set.
32
+
33
+ A geometry feature node also takes input selections (`input`,
34
+ `input2`) as in `feature()`; build the geometry again afterwards. To
35
+ turn on a result selection use `sel.result()` rather than `selresult`,
36
+ because it checks for label clashes. On variables
37
+ (`component.variable()`) any value is taken as an expression, and
38
+ `True` becomes 1.
39
+ """
40
+ owner = container = None
41
+ if isinstance(target, Node):
42
+ if (len(target.path) >= 4 and target.path[0] == 'geometries'
43
+ and not isinstance(target, WorkPlaneNode)):
44
+ # A plain node cannot resolve features inside a work plane.
45
+ target = WorkPlaneNode(target.model, join(target.path))
46
+ java = target.java_if_exists()
47
+ if len(target.path) >= 3 and target.path[0] == 'geometries':
48
+ owner = target.parent()
49
+ container = _comsol.feature_container(owner)
50
+ elif hasattr(target, 'set'):
51
+ java = target
52
+ for key, value in properties.items():
53
+ if value is not None and _comsol.is_selection_input(java, key):
54
+ raise TypeError(
55
+ f'"{key}" is an input selection; use '
56
+ f'java.selection("{key}"), or pass a geometry feature '
57
+ 'as an MPh node.')
58
+ else:
59
+ raise TypeError(f'Cannot set properties of {target!r}; expected an '
60
+ 'MPh node or a COMSOL Java object.')
61
+ _comsol.set_properties(java, properties, owner, container)
62
+ return target