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.
- anisotropy/__init__.py +28 -0
- anisotropy/anisotropy.py +13 -0
- anisotropy/evaluation.py +70 -0
- anisotropy/model.py +200 -0
- anisotropy/predefined.py +202 -0
- anisotropy/py.typed +1 -0
- anisotropy/values.py +84 -0
- mag_material/__init__.py +3 -0
- mag_material/mag_material.py +231 -0
- mag_material/parameters.py +150 -0
- mag_material/py.typed +1 -0
- nmag/__init__.py +36 -0
- nmag/backends.py +493 -0
- nmag/checkpoint.py +327 -0
- nmag/config.py +174 -0
- nmag/demag/__init__.py +39 -0
- nmag/demag/bem_operator.py +149 -0
- nmag/demag/geometry.py +134 -0
- nmag/demag/lindholm.py +133 -0
- nmag/demag/lindholm_fast.py +463 -0
- nmag/demag/linear.py +489 -0
- nmag/dynamics/__init__.py +255 -0
- nmag/output.py +27 -0
- nmag/parallel.py +45 -0
- nmag/py.typed +1 -0
- nmag/resources.py +38 -0
- nmag/simulation/__init__.py +519 -0
- nmag/simulation/anisotropy/__init__.py +5 -0
- nmag/simulation/anisotropy/fields.py +56 -0
- nmag/simulation/anisotropy/materials.py +138 -0
- nmag/simulation/demag/__init__.py +1 -0
- nmag/simulation/demag/bem/__init__.py +11 -0
- nmag/simulation/demag/bem/diagnostics.py +73 -0
- nmag/simulation/demag/bem/dirichlet.py +85 -0
- nmag/simulation/demag/bem/hierarchical.py +74 -0
- nmag/simulation/demag/bem/operator.py +292 -0
- nmag/simulation/demag/fem/__init__.py +11 -0
- nmag/simulation/demag/fem/assembly.py +114 -0
- nmag/simulation/demag/fem/charges.py +38 -0
- nmag/simulation/demag/fem/geometry.py +261 -0
- nmag/simulation/demag/fields/__init__.py +11 -0
- nmag/simulation/demag/fields/auxiliary.py +186 -0
- nmag/simulation/demag/fields/probe.py +77 -0
- nmag/simulation/demag/fields/recovery.py +207 -0
- nmag/simulation/demag/solver.py +17 -0
- nmag/simulation/dynamics/__init__.py +92 -0
- nmag/simulation/dynamics/advance.py +199 -0
- nmag/simulation/dynamics/integrator.py +263 -0
- nmag/simulation/exchange/__init__.py +13 -0
- nmag/simulation/exchange/coefficients.py +185 -0
- nmag/simulation/exchange/fields.py +106 -0
- nmag/simulation/exchange/llg_rhs.py +218 -0
- nmag/simulation/fields/__init__.py +19 -0
- nmag/simulation/fields/arrays.py +111 -0
- nmag/simulation/fields/availability.py +188 -0
- nmag/simulation/fields/averages.py +293 -0
- nmag/simulation/fields/derived.py +164 -0
- nmag/simulation/fields/maxangle.py +163 -0
- nmag/simulation/fields/probes.py +106 -0
- nmag/simulation/implicit_dynamics.py +221 -0
- nmag/simulation/mesh/__init__.py +13 -0
- nmag/simulation/mesh/geometry.py +153 -0
- nmag/simulation/mesh/materials.py +299 -0
- nmag/simulation/mesh/probe.py +234 -0
- nmag/simulation/restart.py +103 -0
- nmag/simulation/support.py +224 -0
- nmag_python_3-0.0.2.dist-info/METADATA +157 -0
- nmag_python_3-0.0.2.dist-info/RECORD +147 -0
- nmag_python_3-0.0.2.dist-info/WHEEL +5 -0
- nmag_python_3-0.0.2.dist-info/licenses/LICENSE +339 -0
- nmag_python_3-0.0.2.dist-info/top_level.txt +8 -0
- nmesh/__init__.py +130 -0
- nmesh/backend.py +286 -0
- nmesh/geometry/__init__.py +52 -0
- nmesh/geometry/boolean_operations.py +157 -0
- nmesh/geometry/primitives.py +453 -0
- nmesh/geometry/transform.py +126 -0
- nmesh/io/__init__.py +50 -0
- nmesh/io/ascii.py +132 -0
- nmesh/io/legacy_nmesh_hdf5.py +318 -0
- nmesh/io/meshio_support.py +170 -0
- nmesh/mesh_generation.py +182 -0
- nmesh/mesh_io.py +227 -0
- nmesh/mesh_model.py +147 -0
- nmesh/mesh_utilities.py +79 -0
- nmesh/mesher/__init__.py +21 -0
- nmesh/mesher/driver.py +146 -0
- nmesh/mesher/meshing_defaults.py +252 -0
- nmesh/mesher/meshing_parameters.py +185 -0
- nmesh/mesher/parity.py +21 -0
- nmesh/mesher/parity_canonical.py +142 -0
- nmesh/mesher/parity_comparison.py +191 -0
- nmesh/mesher/parity_metrics.py +114 -0
- nmesh/mesher/periodic.py +97 -0
- nmesh/mesher/relaxation/__init__.py +14 -0
- nmesh/mesher/relaxation/_constants.py +20 -0
- nmesh/mesher/relaxation/_types.py +15 -0
- nmesh/mesher/relaxation/density.py +170 -0
- nmesh/mesher/relaxation/engine/__init__.py +18 -0
- nmesh/mesher/relaxation/engine/state.py +155 -0
- nmesh/mesher/relaxation/engine/steps.py +248 -0
- nmesh/mesher/relaxation/engine/topology.py +230 -0
- nmesh/mesher/relaxation/forces/__init__.py +96 -0
- nmesh/mesher/relaxation/forces/jit.py +102 -0
- nmesh/mesher/relaxation/forces/neighbors.py +186 -0
- nmesh/mesher/relaxation/forces/simplex.py +302 -0
- nmesh/mesher/relaxation/forces/summary.py +207 -0
- nmesh/mesher/relaxation/forces/types.py +92 -0
- nmesh/mesher/relaxation/geometry/__init__.py +6 -0
- nmesh/mesher/relaxation/geometry/builder.py +154 -0
- nmesh/mesher/relaxation/geometry/model.py +194 -0
- nmesh/mesher/relaxation/seeding/__init__.py +74 -0
- nmesh/mesher/relaxation/seeding/periodic.py +88 -0
- nmesh/mesher/relaxation/seeding/points.py +88 -0
- nmesh/mesher/relaxation/seeding/sampling.py +142 -0
- nmesh/mesher/relaxation/topology/__init__.py +297 -0
- nmesh/mesher/relaxation/topology/finalize.py +78 -0
- nmesh/mesher/relaxation/topology/recovery.py +310 -0
- nmesh/mesher/sectioned_config.py +70 -0
- nmesh/nmesh.py +99 -0
- nmesh/py.typed +1 -0
- nmesh/utils/__init__.py +33 -0
- nmesh/utils/array_list_utils.py +128 -0
- nmesh/utils/constants.py +22 -0
- nmesh/utils/timing_memory_utils.py +51 -0
- nmesh/utils/types.py +13 -0
- si/constants.py +49 -0
- si/physical.py +722 -0
- si/py.typed +1 -0
- simulation/__init__.py +1 -0
- simulation/clock.py +237 -0
- simulation/data_writer.py +273 -0
- simulation/data_writer_collection.py +267 -0
- simulation/hysteresis.py +74 -0
- simulation/hysteresis_runner.py +286 -0
- simulation/hysteresis_schedule.py +180 -0
- simulation/inference/__init__.py +3 -0
- simulation/inference/inference.py +95 -0
- simulation/py.typed +1 -0
- simulation/quantity.py +88 -0
- simulation/simulation_core.py +458 -0
- throttler/__init__.py +3 -0
- throttler/py.typed +1 -0
- throttler/throttler.py +55 -0
- when/__init__.py +3 -0
- when/py.typed +1 -0
- 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
|
+
]
|