nmag-python-3 0.0.2__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.
Files changed (147) hide show
  1. anisotropy/__init__.py +28 -0
  2. anisotropy/anisotropy.py +13 -0
  3. anisotropy/evaluation.py +70 -0
  4. anisotropy/model.py +200 -0
  5. anisotropy/predefined.py +202 -0
  6. anisotropy/py.typed +1 -0
  7. anisotropy/values.py +84 -0
  8. mag_material/__init__.py +3 -0
  9. mag_material/mag_material.py +231 -0
  10. mag_material/parameters.py +150 -0
  11. mag_material/py.typed +1 -0
  12. nmag/__init__.py +36 -0
  13. nmag/backends.py +493 -0
  14. nmag/checkpoint.py +327 -0
  15. nmag/config.py +174 -0
  16. nmag/demag/__init__.py +39 -0
  17. nmag/demag/bem_operator.py +149 -0
  18. nmag/demag/geometry.py +134 -0
  19. nmag/demag/lindholm.py +133 -0
  20. nmag/demag/lindholm_fast.py +463 -0
  21. nmag/demag/linear.py +489 -0
  22. nmag/dynamics/__init__.py +255 -0
  23. nmag/output.py +27 -0
  24. nmag/parallel.py +45 -0
  25. nmag/py.typed +1 -0
  26. nmag/resources.py +38 -0
  27. nmag/simulation/__init__.py +519 -0
  28. nmag/simulation/anisotropy/__init__.py +5 -0
  29. nmag/simulation/anisotropy/fields.py +56 -0
  30. nmag/simulation/anisotropy/materials.py +138 -0
  31. nmag/simulation/demag/__init__.py +1 -0
  32. nmag/simulation/demag/bem/__init__.py +11 -0
  33. nmag/simulation/demag/bem/diagnostics.py +73 -0
  34. nmag/simulation/demag/bem/dirichlet.py +85 -0
  35. nmag/simulation/demag/bem/hierarchical.py +74 -0
  36. nmag/simulation/demag/bem/operator.py +292 -0
  37. nmag/simulation/demag/fem/__init__.py +11 -0
  38. nmag/simulation/demag/fem/assembly.py +114 -0
  39. nmag/simulation/demag/fem/charges.py +38 -0
  40. nmag/simulation/demag/fem/geometry.py +261 -0
  41. nmag/simulation/demag/fields/__init__.py +11 -0
  42. nmag/simulation/demag/fields/auxiliary.py +186 -0
  43. nmag/simulation/demag/fields/probe.py +77 -0
  44. nmag/simulation/demag/fields/recovery.py +207 -0
  45. nmag/simulation/demag/solver.py +17 -0
  46. nmag/simulation/dynamics/__init__.py +92 -0
  47. nmag/simulation/dynamics/advance.py +199 -0
  48. nmag/simulation/dynamics/integrator.py +263 -0
  49. nmag/simulation/exchange/__init__.py +13 -0
  50. nmag/simulation/exchange/coefficients.py +185 -0
  51. nmag/simulation/exchange/fields.py +106 -0
  52. nmag/simulation/exchange/llg_rhs.py +218 -0
  53. nmag/simulation/fields/__init__.py +19 -0
  54. nmag/simulation/fields/arrays.py +111 -0
  55. nmag/simulation/fields/availability.py +188 -0
  56. nmag/simulation/fields/averages.py +293 -0
  57. nmag/simulation/fields/derived.py +164 -0
  58. nmag/simulation/fields/maxangle.py +163 -0
  59. nmag/simulation/fields/probes.py +106 -0
  60. nmag/simulation/implicit_dynamics.py +221 -0
  61. nmag/simulation/mesh/__init__.py +13 -0
  62. nmag/simulation/mesh/geometry.py +153 -0
  63. nmag/simulation/mesh/materials.py +299 -0
  64. nmag/simulation/mesh/probe.py +234 -0
  65. nmag/simulation/restart.py +103 -0
  66. nmag/simulation/support.py +224 -0
  67. nmag_python_3-0.0.2.dist-info/METADATA +157 -0
  68. nmag_python_3-0.0.2.dist-info/RECORD +147 -0
  69. nmag_python_3-0.0.2.dist-info/WHEEL +5 -0
  70. nmag_python_3-0.0.2.dist-info/licenses/LICENSE +339 -0
  71. nmag_python_3-0.0.2.dist-info/top_level.txt +8 -0
  72. nmesh/__init__.py +130 -0
  73. nmesh/backend.py +286 -0
  74. nmesh/geometry/__init__.py +52 -0
  75. nmesh/geometry/boolean_operations.py +157 -0
  76. nmesh/geometry/primitives.py +453 -0
  77. nmesh/geometry/transform.py +126 -0
  78. nmesh/io/__init__.py +50 -0
  79. nmesh/io/ascii.py +132 -0
  80. nmesh/io/legacy_nmesh_hdf5.py +318 -0
  81. nmesh/io/meshio_support.py +170 -0
  82. nmesh/mesh_generation.py +182 -0
  83. nmesh/mesh_io.py +227 -0
  84. nmesh/mesh_model.py +147 -0
  85. nmesh/mesh_utilities.py +79 -0
  86. nmesh/mesher/__init__.py +21 -0
  87. nmesh/mesher/driver.py +146 -0
  88. nmesh/mesher/meshing_defaults.py +252 -0
  89. nmesh/mesher/meshing_parameters.py +185 -0
  90. nmesh/mesher/parity.py +21 -0
  91. nmesh/mesher/parity_canonical.py +142 -0
  92. nmesh/mesher/parity_comparison.py +191 -0
  93. nmesh/mesher/parity_metrics.py +114 -0
  94. nmesh/mesher/periodic.py +97 -0
  95. nmesh/mesher/relaxation/__init__.py +14 -0
  96. nmesh/mesher/relaxation/_constants.py +20 -0
  97. nmesh/mesher/relaxation/_types.py +15 -0
  98. nmesh/mesher/relaxation/density.py +170 -0
  99. nmesh/mesher/relaxation/engine/__init__.py +18 -0
  100. nmesh/mesher/relaxation/engine/state.py +155 -0
  101. nmesh/mesher/relaxation/engine/steps.py +248 -0
  102. nmesh/mesher/relaxation/engine/topology.py +230 -0
  103. nmesh/mesher/relaxation/forces/__init__.py +96 -0
  104. nmesh/mesher/relaxation/forces/jit.py +102 -0
  105. nmesh/mesher/relaxation/forces/neighbors.py +186 -0
  106. nmesh/mesher/relaxation/forces/simplex.py +302 -0
  107. nmesh/mesher/relaxation/forces/summary.py +207 -0
  108. nmesh/mesher/relaxation/forces/types.py +92 -0
  109. nmesh/mesher/relaxation/geometry/__init__.py +6 -0
  110. nmesh/mesher/relaxation/geometry/builder.py +154 -0
  111. nmesh/mesher/relaxation/geometry/model.py +194 -0
  112. nmesh/mesher/relaxation/seeding/__init__.py +74 -0
  113. nmesh/mesher/relaxation/seeding/periodic.py +88 -0
  114. nmesh/mesher/relaxation/seeding/points.py +88 -0
  115. nmesh/mesher/relaxation/seeding/sampling.py +142 -0
  116. nmesh/mesher/relaxation/topology/__init__.py +297 -0
  117. nmesh/mesher/relaxation/topology/finalize.py +78 -0
  118. nmesh/mesher/relaxation/topology/recovery.py +310 -0
  119. nmesh/mesher/sectioned_config.py +70 -0
  120. nmesh/nmesh.py +99 -0
  121. nmesh/py.typed +1 -0
  122. nmesh/utils/__init__.py +33 -0
  123. nmesh/utils/array_list_utils.py +128 -0
  124. nmesh/utils/constants.py +22 -0
  125. nmesh/utils/timing_memory_utils.py +51 -0
  126. nmesh/utils/types.py +13 -0
  127. si/constants.py +49 -0
  128. si/physical.py +722 -0
  129. si/py.typed +1 -0
  130. simulation/__init__.py +1 -0
  131. simulation/clock.py +237 -0
  132. simulation/data_writer.py +273 -0
  133. simulation/data_writer_collection.py +267 -0
  134. simulation/hysteresis.py +74 -0
  135. simulation/hysteresis_runner.py +286 -0
  136. simulation/hysteresis_schedule.py +180 -0
  137. simulation/inference/__init__.py +3 -0
  138. simulation/inference/inference.py +95 -0
  139. simulation/py.typed +1 -0
  140. simulation/quantity.py +88 -0
  141. simulation/simulation_core.py +458 -0
  142. throttler/__init__.py +3 -0
  143. throttler/py.typed +1 -0
  144. throttler/throttler.py +55 -0
  145. when/__init__.py +3 -0
  146. when/py.typed +1 -0
  147. when/when.py +416 -0
nmesh/mesher/driver.py ADDED
@@ -0,0 +1,146 @@
1
+ import logging
2
+ from collections.abc import Callable
3
+ from enum import Enum
4
+ from typing import Any
5
+
6
+ log = logging.getLogger(__name__)
7
+
8
+
9
+ class MeshEngineCommand(Enum):
10
+ """Commands that can be sent to the mesh engine."""
11
+
12
+ DO_STEP = 1 # Execute one relaxation step
13
+ DO_EXTRACT = 2 # Extract intermediate mesh for callback
14
+
15
+
16
+ class MeshEngineStatus(Enum):
17
+ """Status returned by the mesh engine."""
18
+
19
+ FINISHED_STEP_LIMIT_REACHED = 1 # Maximum iteration steps reached
20
+ FINISHED_FORCE_EQUILIBRIUM_REACHED = 2 # Forces converged to equilibrium
21
+ CAN_CONTINUE = 3 # Engine can continue, provides continuation function
22
+ PRODUCED_INTERMEDIATE_MESH = 4 # Intermediate mesh extracted for callback
23
+
24
+
25
+ # Type aliases for improved readability
26
+ EngineFunc = Callable[[MeshEngineCommand], tuple[MeshEngineStatus, Any]]
27
+ Callback = Callable[[int, Any], None]
28
+
29
+
30
+ def do_every_n_steps_driver(
31
+ nr_steps_per_bunch: int, callback: Callback, engine_func: EngineFunc
32
+ ) -> tuple[MeshEngineStatus, Any]:
33
+ """
34
+ Python port of Mesh.do_every_n_steps_driver using an iterative loop.
35
+
36
+ Drives the meshing engine, invoking a callback at regular step intervals.
37
+ Note: The callback is ONLY invoked at multiples of nr_steps_per_bunch during
38
+ the meshing process. When the engine finishes (step limit or equilibrium reached),
39
+ NO final callback is made - the final mesh state is stored in the engine's
40
+ internal state and returned via the status tuple.
41
+
42
+ Args:
43
+ nr_steps_per_bunch: Number of steps between callback invocations
44
+ callback: Function called as callback(step_number, mesh)
45
+ engine_func: Mesh engine function that accepts commands and returns status
46
+
47
+ Returns:
48
+ Final (status, data) tuple when meshing completes
49
+
50
+ Raises:
51
+ ValueError: If nr_steps_per_bunch <= 0 or unknown engine status encountered
52
+ """
53
+ if nr_steps_per_bunch <= 0:
54
+ raise ValueError("nr_steps_per_bunch must be positive")
55
+
56
+ nr_step = 0
57
+ status_out = engine_func(MeshEngineCommand.DO_STEP)
58
+
59
+ while True:
60
+ log.info("do_every_n_steps_driver [%d]", nr_step)
61
+ status, data = status_out
62
+
63
+ if status in (
64
+ MeshEngineStatus.FINISHED_STEP_LIMIT_REACHED,
65
+ MeshEngineStatus.FINISHED_FORCE_EQUILIBRIUM_REACHED,
66
+ ):
67
+ return status_out
68
+
69
+ if status == MeshEngineStatus.CAN_CONTINUE:
70
+ cont = data
71
+ if (nr_step % nr_steps_per_bunch != 0) or nr_step == 0:
72
+ nr_step += 1
73
+ status_out = cont(MeshEngineCommand.DO_STEP)
74
+ continue
75
+
76
+ log.debug("Scheduling Mesh Extraction!")
77
+ status_out = cont(MeshEngineCommand.DO_EXTRACT)
78
+ continue
79
+
80
+ if status == MeshEngineStatus.PRODUCED_INTERMEDIATE_MESH:
81
+ mesh, cont = data
82
+ log.debug("Extracted Mesh!")
83
+ if nr_step != 0:
84
+ callback(nr_step, mesh)
85
+ nr_step += 1
86
+ status_out = cont(MeshEngineCommand.DO_STEP)
87
+ continue
88
+
89
+ raise ValueError(f"Unknown mesh engine status: {status}")
90
+
91
+
92
+ def make_mg_gendriver(
93
+ interval: int, callback: Callable[[int, int, Any], None]
94
+ ) -> Callable[
95
+ [int | EngineFunc],
96
+ tuple[MeshEngineStatus, Any] | Callable[[EngineFunc], tuple[MeshEngineStatus, Any]],
97
+ ]:
98
+ """
99
+ Returns a gendriver using the callback signature for multi-geometry meshing.
100
+
101
+ Creates a driver factory that can handle both single-body and multi-body
102
+ meshing scenarios. When meshing multiple geometric pieces, each piece can
103
+ have its callbacks distinguished by piece number.
104
+
105
+ Args:
106
+ interval: Number of steps between callback invocations
107
+ callback: Function called as callback(piece_number, iteration_step, mesh)
108
+
109
+ Returns:
110
+ A gendriver function that can be called with either:
111
+ - An engine function (for single-body, piece_number defaults to 0)
112
+ - A piece number (returns a driver for that specific piece)
113
+
114
+ Example:
115
+ driver = make_mg_gendriver(100, my_callback)
116
+ # Single body:
117
+ driver(engine_func)
118
+ # Multi-body:
119
+ driver(0)(engine_func_piece_0)
120
+ driver(1)(engine_func_piece_1)
121
+ """
122
+
123
+ def gendriver(
124
+ piece_or_engine: int | EngineFunc,
125
+ ) -> tuple[MeshEngineStatus, Any] | Callable[[EngineFunc], tuple[MeshEngineStatus, Any]]:
126
+ if callable(piece_or_engine):
127
+ # Type checker needs help here - we know it's an EngineFunc
128
+ engine_func: EngineFunc = piece_or_engine
129
+ return do_every_n_steps_driver(
130
+ interval,
131
+ lambda nr_step, mesh: callback(0, nr_step, mesh),
132
+ engine_func,
133
+ )
134
+
135
+ nr_piece = int(piece_or_engine)
136
+
137
+ def driver(engine_func: EngineFunc) -> tuple[MeshEngineStatus, Any]:
138
+ return do_every_n_steps_driver(
139
+ interval,
140
+ lambda nr_step, mesh: callback(nr_piece, nr_step, mesh),
141
+ engine_func,
142
+ )
143
+
144
+ return driver
145
+
146
+ return gendriver
@@ -0,0 +1,252 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from collections.abc import Callable
5
+ from dataclasses import dataclass
6
+ from enum import IntEnum
7
+ from typing import Protocol, TypeAlias
8
+
9
+ from ..utils.constants import MIN_DIVISION_MAGNITUDE
10
+
11
+ log = logging.getLogger(__name__)
12
+
13
+ # Point density constants: Control probabilistic insertion/deletion of mesh points during relaxation.
14
+ DENSITY_ADD_PROBABILITY = 0.1 # 10% chance to add point when density too low
15
+ FORCE_LOW_ADD_PROBABILITY = 0.2 # 20% chance to add when force < threshold
16
+ FORCE_LOW_THRESHOLD = 0.07 # Force threshold below which points may be added
17
+
18
+ DENSITY_DELETE_BASE_PROBABILITY = 0.3 # Base 30% chance to delete when density too high
19
+ DENSITY_DELETE_SLOPE = 0.1 # Additional 10% per unit above threshold
20
+ FORCE_HIGH_DELETE_BASE_PROBABILITY = 0.4 # Base 40% chance to delete when force too high
21
+ FORCE_HIGH_DELETE_SLOPE = 0.1 # Additional 10% per unit above 0.5
22
+ FORCE_HIGH_THRESHOLD = 0.5 # Force threshold above which points may be deleted
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class ParameterSpec:
27
+ """Specification for a meshing parameter with public and internal names.
28
+
29
+ The public_name is the user-friendly API (concise, intuitive).
30
+ The internal_name is the implementation detail (verbose, namespaced).
31
+ This separation is good design even without backward compatibility concerns.
32
+ """
33
+
34
+ public_name: str
35
+ internal_name: str
36
+ default: int | float
37
+ cast: type[int] | type[float]
38
+
39
+
40
+ PUBLIC_PARAMETER_SPECS = (
41
+ ParameterSpec("shape_force_scale", "controller_shape_force_scale", 0.1, float),
42
+ ParameterSpec("volume_force_scale", "controller_volume_force_scale", 0.0, float),
43
+ ParameterSpec("neigh_force_scale", "controller_neigh_force_scale", 1.0, float),
44
+ ParameterSpec(
45
+ "irrel_elem_force_scale",
46
+ "controller_irrel_elem_force_scale",
47
+ 1.0,
48
+ float,
49
+ ),
50
+ ParameterSpec("time_step_scale", "controller_time_step_scale", 0.1, float),
51
+ ParameterSpec("thresh_add", "controller_thresh_add", 1.0, float),
52
+ ParameterSpec("thresh_del", "controller_thresh_del", 2.0, float),
53
+ ParameterSpec("topology_threshold", "controller_topology_threshold", 0.2, float),
54
+ ParameterSpec(
55
+ "tolerated_rel_move",
56
+ "controller_tolerated_rel_movement",
57
+ 0.002,
58
+ float,
59
+ ),
60
+ ParameterSpec("max_steps", "controller_step_limit_max", 1000, int),
61
+ ParameterSpec(
62
+ "initial_settling_steps",
63
+ "controller_initial_settling_steps",
64
+ 100,
65
+ int,
66
+ ),
67
+ ParameterSpec("sliver_correction", "controller_sliver_correction", 1.0, float),
68
+ ParameterSpec(
69
+ "smallest_volume_ratio",
70
+ "controller_smallest_allowed_volume_ratio",
71
+ 1.0,
72
+ float,
73
+ ),
74
+ ParameterSpec("max_relaxation", "controller_movement_max_freedom", 3.0, float),
75
+ ParameterSpec(
76
+ "initial_points_volume_ratio",
77
+ "controller_initial_points_volume_ratio",
78
+ 0.9,
79
+ float,
80
+ ),
81
+ ParameterSpec(
82
+ "splitting_connection_ratio",
83
+ "controller_splitting_connection_ratio",
84
+ 1.6,
85
+ float,
86
+ ),
87
+ ParameterSpec(
88
+ "exp_neigh_force_scale",
89
+ "controller_exp_neigh_force_scale",
90
+ 0.9,
91
+ float,
92
+ ),
93
+ )
94
+
95
+ PUBLIC_PARAMETER_SPECS_BY_NAME = {spec.public_name: spec for spec in PUBLIC_PARAMETER_SPECS}
96
+
97
+ PUBLIC_TO_INTERNAL = {spec.public_name: spec.internal_name for spec in PUBLIC_PARAMETER_SPECS}
98
+ INTERNAL_TO_PUBLIC = {spec.internal_name: spec.public_name for spec in PUBLIC_PARAMETER_SPECS}
99
+
100
+
101
+ class PointFate(IntEnum):
102
+ DO_NOTHING = 0
103
+ ADD_ANOTHER = 1
104
+ DELETE = 2
105
+
106
+
107
+ class SimplexRegion(IntEnum):
108
+ OUTSIDE = 0
109
+ INSIDE = 1
110
+
111
+
112
+ class RandomSource(Protocol):
113
+ def random(self) -> float: ...
114
+
115
+
116
+ RelaxationWeightFunction: TypeAlias = Callable[[int, int, float, float], float]
117
+ ForceFunction: TypeAlias = Callable[[float], float]
118
+ PointDensityHandler: TypeAlias = Callable[
119
+ [RandomSource, tuple[float, float], float, float], PointFate
120
+ ]
121
+ MesherParameter: TypeAlias = (
122
+ int | float | RelaxationWeightFunction | ForceFunction | PointDensityHandler
123
+ )
124
+
125
+
126
+ def default_initial_relaxation_weight(
127
+ iteration_step: int, max_step: int, init_val: float, final_val: float
128
+ ) -> float:
129
+ """Linear function from init_val to final_val, saturating at max_step.
130
+
131
+ Args:
132
+ iteration_step: Current iteration step number
133
+ max_step: Maximum number of steps for interpolation
134
+ init_val: Initial weight value at step 0
135
+ final_val: Final weight value at max_step and beyond
136
+
137
+ Returns:
138
+ Interpolated weight value between init_val and final_val
139
+ """
140
+ if max_step <= 0:
141
+ return final_val
142
+ return init_val + (final_val - init_val) * min(1.0, float(iteration_step) / float(max_step))
143
+
144
+
145
+ def default_relaxation_force_fun(reduced_distance: float) -> float:
146
+ """Repulsing force between two mobile nodes.
147
+
148
+ Args:
149
+ reduced_distance: Distance normalized by ideal neighbor distance
150
+
151
+ Returns:
152
+ Repulsive force magnitude (0.0 if distance > 1.0, else 1.0 - distance)
153
+ """
154
+ if reduced_distance > 1.0:
155
+ return 0.0
156
+ return 1.0 - reduced_distance
157
+
158
+
159
+ def default_boundary_node_force_fun(reduced_distance: float) -> float:
160
+ """Strongly repelling potential for boundary points.
161
+
162
+ This implements a 1/r - 1 potential that enforces strong repulsion
163
+ near boundary nodes to prevent mesh points from violating boundary conditions.
164
+
165
+ Args:
166
+ reduced_distance: Distance normalized by ideal neighbor distance
167
+
168
+ Returns:
169
+ Repulsive force magnitude (very large for small distances, 0.0 if distance > 1.0)
170
+ """
171
+ if reduced_distance > 1.0:
172
+ return 0.0
173
+ if reduced_distance < MIN_DIVISION_MAGNITUDE:
174
+ return 1e12
175
+ return 1.0 / reduced_distance - 1.0
176
+
177
+
178
+ def default_handle_point_density_fun(
179
+ rng: RandomSource,
180
+ avg_stats: tuple[float, float],
181
+ thresh_add: float,
182
+ thresh_del: float,
183
+ ) -> PointFate:
184
+ """Default function to insert or delete points based on density and force.
185
+
186
+ Implements probabilistic point insertion/deletion based on local Voronoi density
187
+ and neighbor force magnitudes. Matches OCaml mdefault_controller_handle_point_density_fun.
188
+
189
+ Args:
190
+ rng: Random number generator with .random() method
191
+ avg_stats: Tuple of (avg_density, avg_force) for the point
192
+ thresh_add: Density threshold below which points may be added
193
+ thresh_del: Density threshold above which points may be deleted
194
+
195
+ Returns:
196
+ PointFate enum: ADD_ANOTHER, DELETE, or DO_NOTHING
197
+ """
198
+ avg_density, avg_force = avg_stats
199
+ if avg_density < thresh_add:
200
+ if rng.random() < DENSITY_ADD_PROBABILITY:
201
+ log.debug("Dtl (dens_avg=%s) - adding point.", avg_density)
202
+ return PointFate.ADD_ANOTHER
203
+ return PointFate.DO_NOTHING
204
+
205
+ if avg_force < FORCE_LOW_THRESHOLD:
206
+ if rng.random() < FORCE_LOW_ADD_PROBABILITY:
207
+ log.debug("Ftl (avg_force=%s) - adding point.", avg_force)
208
+ return PointFate.ADD_ANOTHER
209
+ return PointFate.DO_NOTHING
210
+
211
+ if avg_density > thresh_del:
212
+ prob = DENSITY_DELETE_BASE_PROBABILITY + (avg_density - thresh_del) * DENSITY_DELETE_SLOPE
213
+ if rng.random() < prob:
214
+ log.debug("Dth (dens_avg=%s) - axing point.", avg_density)
215
+ return PointFate.DELETE
216
+ return PointFate.DO_NOTHING
217
+
218
+ if avg_force > FORCE_HIGH_THRESHOLD:
219
+ prob = (
220
+ FORCE_HIGH_DELETE_BASE_PROBABILITY
221
+ + (avg_force - FORCE_HIGH_THRESHOLD) * FORCE_HIGH_DELETE_SLOPE
222
+ )
223
+ if rng.random() < prob:
224
+ log.debug("Fth (avg_force=%s) - axing point.", avg_force)
225
+ return PointFate.DELETE
226
+ return PointFate.DO_NOTHING
227
+
228
+ return PointFate.DO_NOTHING
229
+
230
+
231
+ def _candidate_keys(name: str) -> list[str]:
232
+ """Returns all possible names (public and internal) for a parameter.
233
+
234
+ This allows users to refer to parameters by either their public API name
235
+ or the internal implementation name, whichever is more convenient.
236
+ """
237
+ keys = [name]
238
+ internal = PUBLIC_TO_INTERNAL.get(name)
239
+ public = INTERNAL_TO_PUBLIC.get(name)
240
+
241
+ if internal is not None and internal not in keys:
242
+ keys.append(internal)
243
+ if public is not None and public not in keys:
244
+ keys.append(public)
245
+
246
+ return keys
247
+
248
+
249
+ def _cast_numeric_parameter(spec: ParameterSpec, value: object) -> int | float:
250
+ if not isinstance(value, (str, int, float)):
251
+ raise TypeError(f"Meshing parameter {spec.public_name!r} must be numeric.")
252
+ return spec.cast(value)
@@ -0,0 +1,185 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ from functools import partialmethod
5
+ from os import PathLike
6
+ from typing import Any, cast
7
+
8
+ from ..utils.constants import BOUNDARY_FUZZ, MIN_DIVISION_MAGNITUDE
9
+ from .meshing_defaults import (
10
+ INTERNAL_TO_PUBLIC,
11
+ PUBLIC_PARAMETER_SPECS,
12
+ PUBLIC_PARAMETER_SPECS_BY_NAME,
13
+ PUBLIC_TO_INTERNAL,
14
+ MesherParameter,
15
+ ParameterSpec,
16
+ PointFate,
17
+ SimplexRegion,
18
+ _candidate_keys,
19
+ _cast_numeric_parameter,
20
+ default_boundary_node_force_fun,
21
+ default_handle_point_density_fun,
22
+ default_initial_relaxation_weight,
23
+ default_relaxation_force_fun,
24
+ )
25
+ from .sectioned_config import SectionedConfig
26
+
27
+ __all__ = [
28
+ "MeshingParameters",
29
+ "MesherParameter",
30
+ "ParameterSpec",
31
+ "PointFate",
32
+ "SimplexRegion",
33
+ "default_initial_relaxation_weight",
34
+ "default_relaxation_force_fun",
35
+ "default_boundary_node_force_fun",
36
+ "default_handle_point_density_fun",
37
+ "MIN_DIVISION_MAGNITUDE",
38
+ ]
39
+
40
+
41
+ class MeshingParameters(SectionedConfig):
42
+ def __init__(
43
+ self,
44
+ string: str | None = None,
45
+ file: str | PathLike[str] | None = None,
46
+ ) -> None:
47
+ super().__init__()
48
+ self.dim: int | None = None
49
+ self._setup_defaults()
50
+ if file:
51
+ self.from_file(file)
52
+ if string:
53
+ self.from_string(string)
54
+ self.add_section("user-modifications")
55
+
56
+ def _setup_defaults(self) -> None:
57
+ self._params: dict[str, MesherParameter] = {
58
+ # Volume determination
59
+ "nr_probes_for_determining_volume": 100000,
60
+ # Boundary condition parameters
61
+ "boundary_condition_acceptable_fuzz": BOUNDARY_FUZZ,
62
+ "boundary_condition_max_nr_correction_steps": 200,
63
+ "boundary_condition_debuglevel": 0,
64
+ # Relaxation parameters
65
+ "relaxation_debuglevel": 0,
66
+ "controller_step_limit_min": 500,
67
+ "controller_max_time_step": 10.0,
68
+ # Function-based parameters (callbacks for physics and point management)
69
+ "initial_relaxation_weight_fun": default_initial_relaxation_weight,
70
+ "relaxation_force_fun": default_relaxation_force_fun,
71
+ "boundary_node_force_fun": default_boundary_node_force_fun,
72
+ "handle_point_density_fun": default_handle_point_density_fun,
73
+ }
74
+ self._params.update({spec.internal_name: spec.default for spec in PUBLIC_PARAMETER_SPECS})
75
+
76
+ def _get_section_name(self) -> str:
77
+ if self.dim is None:
78
+ raise RuntimeError("Dimension not set in MeshingParameters")
79
+ return f"nmesh-{self.dim}D" if self.dim in [2, 3] else "nmesh-ND"
80
+
81
+ def _lookup(self, section: str, name: str) -> MesherParameter | None:
82
+ for key in _candidate_keys(name):
83
+ value = self.get(section, key)
84
+ if value is not None:
85
+ return cast(MesherParameter, value)
86
+ return None
87
+
88
+ def _canonical_key(self, name: str) -> str:
89
+ """Converts public API names to internal parameter names.
90
+
91
+ Always stores parameters internally using verbose, namespaced names
92
+ for clarity, even when users provide concise public names.
93
+ """
94
+ internal = PUBLIC_TO_INTERNAL.get(name, name)
95
+ if internal in self._params or name in PUBLIC_TO_INTERNAL:
96
+ return internal
97
+ return name
98
+
99
+ def __getitem__(self, name: str) -> MesherParameter | None:
100
+ user_value = self._lookup("user-modifications", name)
101
+ if user_value is not None:
102
+ return user_value
103
+
104
+ if self.dim is not None:
105
+ section_value = self._lookup(self._get_section_name(), name)
106
+ if section_value is not None:
107
+ return section_value
108
+
109
+ canonical = self._canonical_key(name)
110
+ if canonical in self._params:
111
+ return self._params[canonical]
112
+
113
+ return None
114
+
115
+ def __setitem__(self, key: str, value: MesherParameter) -> None:
116
+ canonical = self._canonical_key(key)
117
+ self._params[canonical] = value
118
+ self.set("user-modifications", canonical, value)
119
+
120
+ def _sync_dimension_section(self, dim: int) -> str:
121
+ """Syncs user modifications to dimension-specific config section.
122
+
123
+ Converts internal parameter names back to public names when writing
124
+ to config sections for user-friendly INI file format.
125
+ """
126
+ self.dim = dim
127
+ section = self._get_section_name()
128
+
129
+ for key, value in self.items("user-modifications"):
130
+ section_key = INTERNAL_TO_PUBLIC.get(key, key)
131
+ self.set(section, section_key, value)
132
+
133
+ return section
134
+
135
+ def to_mesher_config(self, dim: int) -> dict[str, MesherParameter]:
136
+ """Resolves all parameters to internal names for mesher consumption.
137
+
138
+ Returns a dict with internal parameter names, suitable for passing
139
+ to the meshing engine. This keeps the engine code clean and consistent.
140
+ """
141
+ self._sync_dimension_section(dim)
142
+
143
+ resolved: dict[str, MesherParameter] = {}
144
+ for spec in PUBLIC_PARAMETER_SPECS:
145
+ value = self[spec.public_name]
146
+ if value is None:
147
+ continue
148
+ resolved[spec.internal_name] = _cast_numeric_parameter(spec, value)
149
+
150
+ for key, value in self._params.items():
151
+ resolved.setdefault(key, value)
152
+
153
+ return resolved
154
+
155
+ def apply_to_mesher(self, mesher: dict[str, Any], dim: int) -> dict[str, Any]:
156
+ """Applies resolved parameters to mesher config using internal names."""
157
+ self._sync_dimension_section(dim)
158
+ mesher.setdefault("parameters", {})
159
+
160
+ for spec in PUBLIC_PARAMETER_SPECS:
161
+ value = self[spec.public_name]
162
+ if value is None:
163
+ continue
164
+ mesher["parameters"][spec.internal_name] = _cast_numeric_parameter(spec, value)
165
+
166
+ for key, value in self._params.items():
167
+ mesher["parameters"].setdefault(key, value)
168
+
169
+ return mesher
170
+
171
+ def _set_parameter(self, name: str, value: object) -> None:
172
+ """Internal helper for generated setter methods."""
173
+ spec = PUBLIC_PARAMETER_SPECS_BY_NAME[name]
174
+ self[name] = _cast_numeric_parameter(spec, value)
175
+
176
+ def copy(self) -> MeshingParameters:
177
+ return copy.deepcopy(self)
178
+
179
+
180
+ for _spec in PUBLIC_PARAMETER_SPECS:
181
+ setattr(
182
+ MeshingParameters,
183
+ f"set_{_spec.public_name}",
184
+ partialmethod(MeshingParameters._set_parameter, _spec.public_name),
185
+ )
nmesh/mesher/parity.py ADDED
@@ -0,0 +1,21 @@
1
+ """Compatibility exports for mesh parity helpers."""
2
+
3
+ from ..io.ascii import read_ascii_nmesh
4
+ from .parity_canonical import (
5
+ CanonicalMeshSignature,
6
+ assert_canonical_mesh_equal,
7
+ canonical_mesh_signature,
8
+ )
9
+ from .parity_comparison import MeshMetricComparison, compare_mesh_metrics
10
+ from .parity_metrics import MeshMetricSummary, mesh_metric_summary
11
+
12
+ __all__ = [
13
+ "CanonicalMeshSignature",
14
+ "MeshMetricComparison",
15
+ "MeshMetricSummary",
16
+ "assert_canonical_mesh_equal",
17
+ "canonical_mesh_signature",
18
+ "compare_mesh_metrics",
19
+ "mesh_metric_summary",
20
+ "read_ascii_nmesh",
21
+ ]