Invariants¶
These are the narrow rules the code upholds, one per row, each with the code that implements it and the test that guards it. A change that breaks a rule fails the test named beside it; a row marked "no test" relies on review. Principles covers the broader design rules these serve.
Geometry and mesh¶
| Rule | Code | Test |
|---|---|---|
| A multi-body STEP is OCC-fragmented into one conformal decomposition, so touching bodies share interface faces and nodes. | src/femforge/meshing/mesher.py:162-174 |
tests/meshing/test_mesher_2d_integration.py::test_boundaries_are_curve_groups_named_by_adjacency |
| Fragmenting drops STEP names, so each child gets back the name of its smallest-mass input, and an air-magnet overlap belongs to the magnet. | src/femforge/meshing/mesher.py:275-301 |
tests/meshing/test_mesher_2d_integration.py::test_step_faces_keep_their_labels_and_become_body_groups |
| gmsh writes only entities in a physical group, so the boundary partition covers every face exactly once. | src/femforge/meshing/mesher.py:307, src/femforge/meshing/boundary.py:98-129 |
tests/meshing/test_boundary_groups.py::test_partition_is_total_every_face_covered_exactly_once |
A boundary group is named <A>__<B> for an interface, with the body names sorted, <BODY>__EXT for an external shell and <BODY>__AXIS on the symmetry axis of a 2D model. |
src/femforge/meshing/boundary.py:13-20, src/femforge/meshing/boundary.py:112-118 |
tests/meshing/test_boundary_groups.py::test_two_box_partition_yields_interface_and_externals |
The mesher always emits Mesh.RecombineAll = 0, because WhitneyAVSolver needs tetrahedra and a persisted gmsh options file can turn recombination on. |
src/femforge/meshing/gmsh_options.py:92-94 |
tests/meshing/test_gmsh_options.py::test_recombine_all_is_pinned_off |
The global element size applies first, then each region takes the finest EntityTag.mesh_size among its tags. |
src/femforge/meshing/mesh_sizing.py:30-44, src/femforge/meshing/mesh_sizing.py:62-64 |
tests/meshing/test_mesh_sizing.py::test_resolve_finest_requested_size_wins |
MeshConfig.deterministic pins gmsh to one thread, whatever num_threads says, so the same STEP and sizes yield the identical mesh. |
src/femforge/meshing/config.py:27-34 |
tests/meshing/test_gmsh_options.py::test_deterministic_pins_meshing_to_one_thread |
ElmerGrid renumbers the groups, so reconcile_ids rewrites each group's id from mesh.names before the sif targets it. |
src/femforge/elmer/mesh_names.py:64-83, src/femforge/run/driver.py:617-623 |
tests/elmer/test_mesh_names.py::test_reconcile_overwrites_gmsh_tags_with_elmer_ids |
| The axisymmetric frame puts the radial direction on X and the symmetry axis on Y, and a section that reaches X < 0 raises, since Elmer rejects it silently. | src/femforge/common/frame.py:83, src/femforge/geometry/section.py:121 |
tests/geometry/test_section.py::test_a_section_leaking_below_x_zero_raises |
Solver input¶
| Rule | Code | Test |
|---|---|---|
A body's boundary conditions project onto every surface group the body borders, and an external_only condition lands only on an external shell. |
src/femforge/elmer/sim.py:410-422 |
tests/elmer/test_sif_smoke.py::test_fixed_temperature_reaches_only_the_board_surface |
| A surface group gets one merged Boundary block, because Elmer honors a single BC section per boundary element. | src/femforge/elmer/sim.py:377-397 |
tests/elmer/test_sif_smoke.py::test_one_boundary_block_per_group_with_conditions_merged |
Magnetic physics pins the far field to zero on external shells: AV {e} in 3D, the nodal Potential in 2D. |
src/femforge/elmer/sim.py:427-435 |
tests/elmer/test_sif_smoke.py::test_axisymmetric_2d_far_field_pins_potential_not_av |
Each body's material must emit the union of the required_material_keys of the solvers acting on it; a conductor_only solver's keys apply only to conductors. |
src/femforge/elmer/sim.py:143-160 |
tests/elmer/test_sif_validation.py::test_missing_required_material_property_raises |
A magnet's Magnetization components go on its own Material block, which WhitneyAVSolver reads, never on a Body Force. |
src/femforge/elmer/sim.py:445-468 |
tests/elmer/test_sif_smoke.py::test_magnet_body_gets_magnetization_on_material |
| Magnetostatics solves with BiCGStabl of degree 4 and no preconditioning, since the gauge-non-unique AV system stalls under ILU. | src/femforge/elmer/config/electromagnetism/magnetodynamics.py:55-57 |
tests/elmer/test_solvers.py::test_magnetodynamics_block_is_byte_compatible |
| StatCurrent runs on conductor bodies only and solves Direct with umfpack; dielectrics take a second equation without it. | src/femforge/elmer/config/electromagnetism/stat_current.py:44-60, src/femforge/elmer/sim.py:236-244 |
tests/elmer/test_sif_current.py::test_stat_current_solver_defaults_to_direct_umfpack |
| Partitioning adds halo elements only for the coil stack, because halos skew the force lumping of partition-cut bodies. | src/femforge/elmer/config/common/physics_config.py:91-93 |
tests/elmer/test_solvers.py::test_needs_halobc_only_for_the_coil_stack |
| The sif writer reproduces pyelmer's output text and always writes LF line endings. | src/femforge/elmer/sif.py:1, src/femforge/elmer/sif.py:393-396 |
tests/elmer/test_sif_writer.py::test_case_sif_matches_reference |
Units and config¶
| Rule | Code | Test |
|---|---|---|
A material property stays a pint quantity until to_elmer converts it to its Elmer unit and strips it to a float. |
src/femforge/materials/properties.py:59-63, src/femforge/elmer/sim.py:162-167 |
tests/elmer/test_sif_validation.py::test_leaked_pint_quantity_raises |
A physical field's docstring opens with `glyph` `[unit]`: meaning, and pint parses the unit. |
src/femforge/meshing/config.py:21-22 |
tests/test_field_docstrings.py::test_physical_field_docstring_format |
Vec3 compares and hashes its exact components; Direction normalizes and rounds to 12 digits, so scaled copies compare equal. |
src/femforge/common/vector.py:122-142, src/femforge/common/vector.py:207-228 |
tests/vector/test_vec3.py::test_direction_is_normalized_and_scaled_copies_compare_and_hash_equal |
Runs and artifacts¶
| Rule | Code | Test |
|---|---|---|
| Execution settings never enter the run identity: the backend, the mesher, the rank count, retention and telemetry. | src/femforge/run/driver.py:128 |
tests/run/test_driver.py::test_driver_mpi_processes_is_not_part_of_the_run_identity |
cache_key hashes the config without meta, the STEP path and physics.overrides, plus the STEP key, the resolved solver stack and RUN_PRODUCER_VERSION, so overrides equal to the preset leave it unchanged. |
src/femforge/run/kinds.py:135-147 |
tests/run/test_manifest.py::test_cache_key_ignores_overrides_identical_to_the_preset |
Every artifact key hashes the kind name, its producer version and the spec without its HashExclude fields at any depth. |
src/femforge/artifacts/keys.py:91-105 |
tests/artifacts/test_keys.py::test_keys_are_stable_and_ignore_hash_excluded_fields_at_any_depth |
The mesh key hashes the MeshSpec, so materials' properties, conditions, physics and metadata never move it. |
src/femforge/run/kinds.py:108-132 |
tests/run/test_kinds.py::test_mesh_key_ignores_everything_that_only_changes_the_solve |
An artifact exists exactly when its artifact.json sidecar does, so the store writes the sidecar last, inside the staging directory, before the rename. |
src/femforge/artifacts/sidecar.py:19-20, src/femforge/artifacts/local.py:242-245 |
tests/artifacts/test_local_store.py::test_put_from_staging_prunes_unlisted_files_and_commits_atomically |
A record's name sits beside its key and is always derived from the key, never read from input. |
src/femforge/artifacts/sidecar.py:80-81, src/femforge/artifacts/sidecar.py:107-110 |
tests/artifacts/test_sidecar.py::test_record_name_is_derived_and_survives_a_round_trip |
| Publishing is first-wins: the losing publisher's rename fails and its staging copy is discarded. | src/femforge/artifacts/local.py:36-38, src/femforge/artifacts/local.py:244-249 |
tests/artifacts/test_local_store.py::test_two_publishers_racing_on_one_key |
| A producer holds a heartbeat claim; a claim without a heartbeat for 120 s goes stale and another claimant breaks it. | src/femforge/artifacts/lock.py:26-29, src/femforge/artifacts/lock.py:84-91 |
tests/artifacts/test_lock.py::test_a_claim_without_heartbeats_goes_stale_and_is_broken |
| The default retention removes only store-sourced inputs, and no policy removes the manifest or the result. | src/femforge/run/retention.py:100, src/femforge/run/retention.py:338-339 |
tests/run/test_retention.py::test_default_policy_removes_nothing_unless_cached |
| Telemetry is append-only: each append writes a new shard per partition, and each process journal flushes every record before parquet sees it. | src/femforge/telemetry/hive.py:149-162, src/femforge/telemetry/journal.py:146-155 |
tests/telemetry/test_hive.py::test_each_append_writes_a_distinct_shard |
Results¶
| Rule | Code | Test |
|---|---|---|
| An MPI solve's scalars are read from rank 0 and never summed, since every rank writes the globally reduced row. | src/femforge/run/parse.py:98-122 |
tests/run/test_parse.py::test_parse_mpi_rank_dats_read_rank_zero |
An axisymmetric result scales Elmer's per-radian forces by 2π, reports only the axial component and keeps raw verbatim. |
src/femforge/run/magnetic.py:39-41, src/femforge/run/parse.py:214-217 |
tests/run/test_parse.py::test_parse_axisymmetric_revolves_the_typed_vectors |
| The field display's automatic range is the union of every body's own robust range, hidden or styled bodies included. | src/femforge/viz/field_display.py:369-410, src/femforge/viz/field_display.py:790-792 |
tests/viz/test_field_display.py::test_the_color_range_covers_every_body_however_it_is_shown |
| A field line ends at its first sharp reversal, where the cosine between consecutive steps drops below -0.5. | src/femforge/viz/field_lines.py:222-259 |
tests/viz/test_field_lines.py::test_a_line_ends_at_its_first_sharp_reversal_and_a_straight_one_stays_whole |
Workbench¶
| Rule | Code | Test |
|---|---|---|
| Field lines follow the shown field when it has three components, else the magnetic flux density. | packages/femforge-workbench/src/femforge_workbench/panels/field_viewer.py:993-1003 |
tests/workbench/test_field_viewer.py::test_the_field_lines_follow_the_shown_vector_field_and_fall_back_to_the_flux_density |
| A page has one VTK.wasm mirror: a panel that switches to the mirror moves every older mirroring panel to streaming. | packages/femforge-workbench/src/femforge_workbench/panels/scene.py:523-528, packages/femforge-workbench/src/femforge_workbench/panels/scene.py:677-682 |
tests/workbench/test_scene.py::test_the_newest_panel_takes_the_mirror_and_the_older_ones_stream |
| Mirrored polydata carries 32-bit cell arrays, because the wasm32 runtime misreads 64-bit ids. | packages/femforge-workbench/src/femforge_workbench/panels/scene.py:221-230 |
tests/workbench/test_scene.py::test_the_mirror_gets_cell_arrays_its_32_bit_runtime_reads |
Packaging and imports¶
| Rule | Code | Test |
|---|---|---|
import femforge stays under 60 ms and loads no femforge subpackage, pydantic, pint, numpy, loguru or rich. |
src/femforge/__init__.py:3-8 |
tests/test_import_time.py::test_import_femforge_loads_no_heavy_dependency |
The first SimulationConfig loads no gmsh, build123d, polars, VTK, workbench or run stack, so femforge.meshing exports neither Mesher nor MeshEntity, which import gmsh. |
src/femforge/meshing/__init__.py:5-13, src/femforge/meshing/mesher.py:8, src/femforge/meshing/mesh_entity.py:3 |
tests/test_import_time.py::test_the_first_simulation_config_loads_no_cad_viz_or_run_stack |
A driver that meshes through WorkerMesher never loads gmsh in its own process, whether it meshes, produces a cached mesh or rebinds one. |
src/femforge/meshing/backend.py:204-205, src/femforge/meshing/firewall.py:28-34 |
tests/run/test_gmsh_firewall.py::test_a_worker_driver_never_loads_gmsh |
A lazy package's __init__.pyi is its only export list: dir() and __all__ equal the stub's names. |
src/femforge/__init__.py:8 |
tests/test_public_api.py::test_dir_of_a_lazy_package_equals_its_stub |
A stub holds only one-dot relative imports and ASCII text, because lazy_loader.attach_stub rejects any other import level and reads the file in the locale encoding. |
src/femforge/__init__.pyi:1 |
no test |
A public callable with three or more parameters documents each one in a Google Args: block. |
src/femforge/meshing/naming.py:22-26 |
tests/test_public_api.py::test_a_public_callable_documents_its_arguments |
| femforge and femforge-workbench carry the same version, and each pins the other at it. | src/femforge/__init__.py:6, packages/femforge-workbench/pyproject.toml:3 |
tests/test_release.py::test_package_versions_match |