Repository: magpylib/magpylib (language: python)

Issue:
Add `field_func` attribute to core sources
As discussed, Magpylib objects should have the field function stored as an attribute.

This would make it very easy to create custom sources with custom object attribute dependent field functions

This would not break the API.

When this is done also review the CustomSource examples and use the dynamic field function option in the quadrupole example


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ef3d52147..5476c8139 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,7 +15,7 @@ This is a major update that includes
 - `Box` class renamed to `Cuboid`. ([#350](https://github.com/magpylib/magpylib/issues/350))
 - `Circular` class renamed to `Loop`. ([#402](https://github.com/magpylib/magpylib/pull/402))
 - New `CylinderSegment` class with dimension `(r1,r2,h,phi1,phi2)` with the inner radius `r1`, the outer radius `r2` the height `h` and the cylinder section angles `phi1 < phi2`. ([#386](https://github.com/magpylib/magpylib/issues/386), [#385](https://github.com/magpylib/magpylib/issues/385), [#484](https://github.com/magpylib/magpylib/pull/484), [#480](https://github.com/magpylib/magpylib/issues/480))
-- New `CustomSource` class for user defined field functions ([#349](https://github.com/magpylib/magpylib/issues/349), [#409](https://github.com/magpylib/magpylib/issues/409), [#411](https://github.com/magpylib/magpylib/pull/411))
+- New `CustomSource` class for user defined field functions ([#349](https://github.com/magpylib/magpylib/issues/349), [#409](https://github.com/magpylib/magpylib/issues/409), [#411](https://github.com/magpylib/magpylib/pull/411), [#506](https://github.com/magpylib/magpylib/pull/506))
 - All Magpylib objects can now be initialized without excitation and dimension attributes.
 - All classes now have the `parent` attribute to reference to a collection they are part of. Any object can only have a single parent.
 - All classes have the `describe` method which gives a quick object property overview.
@@ -75,7 +75,7 @@ This is a major update that includes
 - Removed `increment` argument from `move` and `rotate` functions ([#438](https://github.com/magpylib/magpylib/discussions/438), [#444](https://github.com/magpylib/magpylib/issues/444))
 
 ### Modifications to the `Collection` class
-- Collections can now contain source, `Sensor` and other `Collection` objects and can function as source and observer inputs in `getB` and `getH`. ([#502](https://github.com/magpylib/magpylib/pull/502), [#410](https://github.com/magpylib/magpylib/issues/410), [#415](https://github.com/magpylib/magpylib/pull/415), [#297](https://github.com/magpylib/magpylib/issues/297))
+- Collections can now contain `Source`, `Sensor` and other `Collection` objects and can function as source and observer inputs in `getB` and `getH`. ([#502](https://github.com/magpylib/magpylib/pull/502), [#410](https://github.com/magpylib/magpylib/issues/410), [#415](https://github.com/magpylib/magpylib/pull/415), [#297](https://github.com/magpylib/magpylib/issues/297))
 - Instead of the property `Collection.sources` there are now the `Collection.children`, `Collection.sources`, `Collection.sensors` and `Collection.collections` properties. Setting these collection properties will automatically override parents. ([#446](https://github.com/magpylib/magpylib/issues/446), [#502](https://github.com/magpylib/magpylib/pull/502))
 - `Collection` has it's own `position`, `orientation` and `style`. ([#444](https://github.com/magpylib/magpylib/issues/444), [#461](https://github.com/magpylib/magpylib/issues/461))
 - All methods applied to a collection maintain relative child-positions in the local reference frame.
diff --git a/docs/_pages/page_01_introduction.md b/docs/_pages/page_01_introduction.md
index f9f99216b..7c1a3bba9 100644
--- a/docs/_pages/page_01_introduction.md
+++ b/docs/_pages/page_01_introduction.md
@@ -109,7 +109,7 @@ All current objects have the `current` attribute which must be a scalar $i_0$ an
 
 - **`Dipole`**`(moment, position, orientation)` represents a magnetic dipole moment with moment $(m_x,m_y,m_z)$ given in \[mT mm³]. For homogeneous magnets the relation moment=magnetization$\times$volume holds. Can be used as Magpylib `sources` input.
 
-- **`CustomSource`**`(field_B_lambda, field_H_lambda, position, orientation)` is used to create user defined custom sources with their own field functions. Can be used as Magpylib `sources` input.
+- **`CustomSource`**`(field_func, position, orientation)` is used to create user defined custom sources with their own field functions. Can be used as Magpylib `sources` input.
 
 - **`Sensor`**`(position, pixel, orientation)` represents a 3D magnetic field sensor. The field is evaluated at the given pixel positions. By default (`pixel=(0,0,0)`) the pixel position coincide in the local object coordinates and the global coordinates. Can be used as Magpylib `observers` input.
 
@@ -451,9 +451,9 @@ import magpylib as magpy
 
 mag = np.array([(100,0,0)]*5)
 dim = np.array([(1,2,3,45,90)]*5)
-pos = np.array([(0,0,0)]*5)
+obs = np.array([(0,0,0)]*5)
 
-B = magpy.core.magnet_cylinder_segment_field(mag, dim, pos, field='B')
+B = magpy.core.magnet_cylinder_segment_field('B', obs, mag, dim)
 print(B)
 ```
 
@@ -541,7 +541,7 @@ One central motivation behind the `Collection` class is enabling users to build
 
 **User-defined 3D models** (traces) for any object that will be displayed by `show`, can be stored in `style.model3d.data`. A trace itself is a dictionary that contains all information necessary for plotting, and can be added with the method `style.model3d.data.add_trace`. In the example gallery {ref}`examples-3d-models` it is explained how to create custom traces with standard plotting backends such as `scatter3d` or `mesh3d` in Plotly, or `plot`, `plot_surface` and `plot_trisurf` in Matplotlib. Some pre-defined models are also provided for easy parts visualization.
 
-**User-defined source objects** are easily realized with the `CustomSource` class. Such a custom source object is provided with user-defined field computation functions, that are stored in the attributes `field_B_lambda` and `field_H_lambda`, and will be used when `getB` and `getH` are called. The provided functions must accept position arrays with shape (n,3), and return the field with a similar shape. Details on working with custom sources are given in {ref}`examples-custom-source-objects`.
+**User-defined source objects** are easily realized with the `CustomSource` class. Such a custom source object is provided with a user-defined field computation function, that is stored in the attribute `field_func` and is used when `getB` and `getH` are called. Similar to core functions, `field_func` must have the two positional arguments `field` (can be `'B'` or `'H'`) and `observers` (must accept ndarrays of shape (n,3)), and return the respective fields in units of \[mT\] and \[kA/m\] in the same shape. Details on working with custom sources are given in {ref}`examples-custom-source-objects`.
 
 While each of these features can be used individually, the combination of the two (own source class with own 3D representation) enables a high level of customization in Magpylib. Such user-defined objects will feel like native Magpylib objects and can be used in combination with all other features, which is demonstrated in the following example:
 
@@ -550,15 +550,15 @@ import numpy as np
 import plotly.graph_objects as go
 import magpylib as magpy
 
-# define B-field function for custom source
-def easter_field(pos):
+# define B/H field function for custom source
+def easter_field(field, observers):
     """ points in z-direction and decays with 1/r^3"""
-    dist = np.linalg.norm(pos, axis=1)
-    return np.c_[np.zeros((len(pos),2)), 1/dist**3]
+    dist = np.linalg.norm(observers, axis=1)
+    return np.c_[np.zeros((len(observers),2)), 1/dist**3]
 
 # create custom source
 egg = magpy.misc.CustomSource(
-    field_B_lambda=easter_field,
+    field_func=easter_field,
     style=dict(color='orange', model3d_showdefault=False, label='The Egg'),
 )
 
diff --git a/docs/examples/examples_04_custom_source.md b/docs/examples/examples_04_custom_source.md
index dd6c9c5a2..613a6889a 100644
--- a/docs/examples/examples_04_custom_source.md
+++ b/docs/examples/examples_04_custom_source.md
@@ -14,30 +14,32 @@ kernelspec:
 (examples-custom-source-objects)=
 # Custom source objects
 
-The class `CustomSource` allows users to integrate their own custom-objects into the Magpylib interface. The `field_B_lambda` and `field_H_lambda` arguments can be provided with function that are called with `getB` and `getH`.
-
-These custom field functions are treated like core functions. They must accept position inputs (array_like, shape (n,3)) and return the respective field with a similar shape. A fundamental example how to create a custom source object is:
+The class `CustomSource` allows users to integrate their own custom-object field computation into the Magpylib interface. For this, the argument `field_func` must be provided with a function that is then automatically called with `getB` and `getH`. This custom field function is treated like a core function. It must have the positional arguments `field` (with values `'B'` or `'H'`), and `observers` (must accept array_like, shape (n,3)) and return the B-field in units of \[mT\] and the H-field in units of \[kA/m\] with a similar shape. A fundamental example how to create a custom source object is shown below:
 
 ```{code-cell} ipython3
 import numpy as np
 import magpylib as magpy
 
 # define field function
-def custom_field(position):
+def custom_field(field, observers):
     """ user defined custom field
     position input: array_like, shape (n,3)
     returns: B-field, ndarray, shape (n,3)
     """
-    return np.array(position)*2
+    if field=='B':
+        return np.array(observers)*2
+    return np.array(observers)
 
 # custom source
-source = magpy.misc.CustomSource(field_B_lambda=custom_field)
+source = magpy.misc.CustomSource(field_func=custom_field)
 
 # compute field with 2-pixel sensor
 sensor = magpy.Sensor(pixel=((1,1,1), (2,2,2)))
 
 B = magpy.getB(source, sensor)
 print(B)
+H = magpy.getH(source, sensor)
+print(H)
 ```
 
 Custom sources behave like native Magpylib objects. They can make full use of the geometry interface, have style properties, and can be part of collections and field computation together with all other source types. A custom 3D representation for display in `show` can be provided via the `style.model3d` attribute, see {ref}`examples-own-3d-models`.
@@ -65,15 +67,15 @@ trace_pole = magpy.graphics.model3d.make_Ellipsoid(
 # combine four monopole custom sources into a quadrupole collection
 def create_pole(charge):
     """ create a monopole object"""
-    field = lambda x: monopole_field( charge, x)
+    field = lambda field, observers: monopole_field(charge, observers)
     monopole = magpy.misc.CustomSource(
-        field_B_lambda=field,
+        field_func=field,
         style_model3d_showdefault=False,
     )
     monopole.style.model3d.add_trace(trace_pole)
     return monopole
 
-quadrupole = magpy.Collection([create_pole(q) for q in [1,1,-1,-1]])
+quadrupole = magpy.Collection(*[create_pole(q) for q in [1,1,-1,-1]])
 
 # move and color the pole objects
 pole_pos = np.array([(1,0,0), (-1,0,0), (0,0,1), (0,0,-1)])
diff --git a/docs/examples/examples_22_field_interpolation.md b/docs/examples/examples_22_field_interpolation.md
index 4659641a9..d6a3f790e 100644
--- a/docs/examples/examples_22_field_interpolation.md
+++ b/docs/examples/examples_22_field_interpolation.md
@@ -13,7 +13,7 @@ kernelspec:
 
 # Field interpolation
 
-Working with complex magnet shapes can be cumbersome when a lot of base shapes are required, see {ref}`examples-complex-forms`. One way around this problem is to compute the field only a single time on a 3D grid, define an interpolation function, and use the interpolation function as `field_B_lambda` input of a custom source.
+Working with complex magnet shapes can be cumbersome when a lot of base shapes are required, see {ref}`examples-complex-forms`. One way around this problem is to compute the field only a single time on a 3D grid, define an interpolation function, and use the interpolation function as `field_func` input of a custom source.
 
 This makes it possible to use the full Magpylib geometry interface, to move/rotate the complex source around, without having to recompute the total field at new observer positions. Of course this method would also allow it to integrate the field from a **finite element** computation or some **experimental data** into Magpylib.
 
@@ -65,10 +65,13 @@ def interpolate_field(data, method="linear", bounds_error=False, fill_value=np.n
     # construct interpolation with RegularGridInterpolator
     field_interp = []
     kwargs = dict(bounds_error=bounds_error, fill_value=fill_value, method=method)
-    for field in field_vec:
-        rgi = RegularGridInterpolator((X, Y, Z), field.reshape(nx, ny, nz), **kwargs)
+    for f in field_vec:
+        rgi = RegularGridInterpolator((X, Y, Z), f.reshape(nx, ny, nz), **kwargs)
         field_interp.append(rgi)
-    return lambda x: np.array([field(x) for field in field_interp]).T
+
+    def field_func(field, observers):
+        return np.array([f(observers) for f in field_interp]).T
+    return field_func
 ```
 
 ## Custom source with interpolation field
@@ -83,7 +86,7 @@ grid = np.array(np.meshgrid(ts,ts,ts)).T.reshape(-1,3)
 data = np.hstack((grid, cube.getB(grid)))
 
 # create custom source with nice 3D model
-custom = magpy.misc.CustomSource(field_B_lambda=interpolate_field(data))
+custom = magpy.misc.CustomSource(field_func=interpolate_field(data))
 
 xs = 1.1*np.array([-1, -1,  1,  1, -1, -1, -1, -1, -1,  1,  1,  1,  1, 1,  1, -1])
 ys = 1.1*np.array([-1,  1,  1, -1, -1, -1,  1,  1,  1,  1,  1,  1, -1, -1, -1, -1])
diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py
index 93f41a749..c4aa62d1c 100644
--- a/magpylib/_src/fields/field_BH_cuboid.py
+++ b/magpylib/_src/fields/field_BH_cuboid.py
@@ -7,10 +7,10 @@
 from magpylib._src.input_checks import check_field_input
 
 def magnet_cuboid_field(
+    field: str,
+    observers: np.ndarray,
     magnetization: np.ndarray,
-    dimension: np.ndarray,
-    observer: np.ndarray,
-    field='B') -> np.ndarray:
+    dimension: np.ndarray) -> np.ndarray:
     """Magnetic field of a homogeneously magnetized cuboid.
 
     The cuboid sides are parallel to the coordinate axes. The geometric center of the
@@ -18,19 +18,19 @@ def magnet_cuboid_field(
 
     Parameters
     ----------
+    field: str, default=`'B'`
+        If `field='B'` return B-field in units of [mT], if `field='H'` return H-field
+        in units of [kA/m].
+
+    observers: ndarray, shape (n,3)
+        Observer positions (x,y,z) in Cartesian coordinates in units of [mm].
+
     magnetization: ndarray, shape (n,3)
         Homogeneous magnetization vector in units of [mT].
 
     dimension: ndarray, shape (n,3)
         Cuboid side lengths in units of [mm].
 
-    observer: ndarray, shape (n,3)
-        Observer positions (x,y,z) in Cartesian coordinates in units of [mm].
-
-    field: str, default=`'B'`
-        If `field='B'` return B-field in units of [mT], if `field='H'` return H-field
-        in units of [kA/m].
-
     Returns
     -------
     B-field or H-field: ndarray, shape (n,3)
@@ -81,7 +81,7 @@ def magnet_cuboid_field(
 
     magx, magy, magz = magnetization.T
     a, b, c = np.abs(dimension.T)/2
-    x, y, z = observer.T
+    x, y, z = observers.T
 
     # This implementation is completely scale invariant as only observer/dimension
     # ratios appear in equations below.
@@ -123,7 +123,7 @@ def magnet_cuboid_field(
     if np.any(mask_gen):
         magx, magy, magz = magnetization[mask_gen].T
         a, b, c = dimension[mask_gen].T/2
-        x, y, z = np.copy(observer[mask_gen]).T
+        x, y, z = np.copy(observers[mask_gen]).T
 
 
         # avoid indeterminate forms by evaluating in bottQ4 only --------
diff --git a/magpylib/_src/fields/field_BH_cylinder.py b/magpylib/_src/fields/field_BH_cylinder.py
index 81b94d927..8dac1bd6c 100644
--- a/magpylib/_src/fields/field_BH_cylinder.py
+++ b/magpylib/_src/fields/field_BH_cylinder.py
@@ -203,10 +203,10 @@ def fieldH_cylinder_diametral(
 
 # ON INTERFACE
 def magnet_cylinder_field(
+    field: str,
+    observers: np.ndarray,
     magnetization: np.ndarray,
     dimension: np.ndarray,
-    observer: np.ndarray,
-    field='B',
     ) -> np.ndarray:
     """Magnetic field of a homogeneously magnetized cylinder.
 
@@ -215,6 +215,13 @@ def magnet_cylinder_field(
 
     Parameters
     ----------
+    field: str, default=`'B'`
+        If `field='B'` return B-field in units of [mT], if `field='H'` return H-field
+        in units of [kA/m].
+
+    observers: ndarray, shape (n,3)
+        Observer positions (x,y,z) in Cartesian coordinates in units of [mm].
+
     magnetization: ndarray, shape (n,3)
         Homogeneous magnetization vector in units of [mT].
 
@@ -256,7 +263,7 @@ def magnet_cylinder_field(
     bh = check_field_input(field, 'magnet_cylinder_field()')
 
     # transform to Cy CS --------------------------------------------
-    r, phi, z = cart_to_cyl_coordinates(observer)
+    r, phi, z = cart_to_cyl_coordinates(observers)
     r0,z0 = dimension.T/2
 
     # scale invariance (make dimensionless)
diff --git a/magpylib/_src/fields/field_BH_cylinder_segment.py b/magpylib/_src/fields/field_BH_cylinder_segment.py
index 4ef2a7b3a..3c8fb9b60 100644
--- a/magpylib/_src/fields/field_BH_cylinder_segment.py
+++ b/magpylib/_src/fields/field_BH_cylinder_segment.py
@@ -1350,10 +1350,10 @@ def magnet_cylinder_segment_core(
 
 
 def magnet_cylinder_segment_field(
+    field: str,
+    observers: np.ndarray,
     magnetization: np.ndarray,
     dimension: np.ndarray,
-    observer: np.ndarray,
-    field='B',
     ) -> np.ndarray:
     """Magnetic field of a homogeneously magnetized cylinder segment.
 
@@ -1362,6 +1362,13 @@ def magnet_cylinder_segment_field(
 
     Parameters
     ----------
+    field: str, default=`'B'`
+        If `field='B'` return B-field in units of [mT], if `field='H'` return H-field
+        in units of [kA/m].
+
+    observers: ndarray, shape (n,3)
+        Observer positions (x,y,z) in Cartesian coordinates in units of [mm].
+
     magnetization: ndarray, shape (n,3)
         Homogeneous magnetization vector in units of [mT].
 
@@ -1369,13 +1376,6 @@ def magnet_cylinder_segment_field(
         Cylinder segment dimensions (r1,r2,h,phi1,phi2) with inner radius r1, outer radius r2,
         height h in units of [mm] and the two segment angles phi1 and phi2 in units of [deg].
 
-    observer: ndarray, shape (n,3)
-        Observer positions (x,y,z) in Cartesian coordinates in units of [mm].
-
-    field: str, default=`'B'`
-        If `field='B'` return B-field in units of [mT], if `field='H'` return H-field
-        in units of [kA/m].
-
     Returns
     -------
     B-field or H-field: ndarray, shape (n,3)
@@ -1415,7 +1415,7 @@ def magnet_cylinder_segment_field(
     dim = np.array([r1, r2, phi1, phi2, z1, z2]).T
 
     # transform obs_pos to Cy CS --------------------------------------------
-    x, y, z = observer.T
+    x, y, z = observers.T
     r, phi = np.sqrt(x**2+y**2), np.arctan2(y, x)
     pos_obs_cy = np.concatenate(((r,),(phi,),(z,)),axis=0).T
 
diff --git a/magpylib/_src/fields/field_BH_dipole.py b/magpylib/_src/fields/field_BH_dipole.py
index 1c37973b3..3f03f53fd 100644
--- a/magpylib/_src/fields/field_BH_dipole.py
+++ b/magpylib/_src/fields/field_BH_dipole.py
@@ -7,9 +7,9 @@
 
 
 def dipole_field(
+    field: str,
+    observers: np.ndarray,
     moment: np.ndarray,
-    observer: np.ndarray,
-    field='B'
     ) -> np.ndarray:
     """Magnetic field of a dipole moment.
 
@@ -17,16 +17,16 @@ def dipole_field(
 
     Parameters
     ----------
-    moment: ndarray, shape (n,3)
-        Dipole moment vector in units of [mT*mm^3].
-
-    observer: ndarray, shape (n,3)
-        Observer positions (x,y,z) in Cartesian coordinates in units of [mm].
-
     field: str, default=`'B'`
         If `field='B'` return B-field in units of [mT], if `field='H'` return H-field
+
+    observers: ndarray, shape (n,3)
+        Observer positions (x,y,z) in Cartesian coordinates in units of [mm].
         in units of [kA/m].
 
+    moment: ndarray, shape (n,3)
+        Dipole moment vector in units of [mT*mm^3].
+
     Returns
     -------
     B-field or H-field: ndarray, shape (n,3)
@@ -49,12 +49,12 @@ def dipole_field(
     """
     bh = check_field_input(field, 'dipole_field()')
 
-    x, y, z = observer.T
+    x, y, z = observers.T
     r = np.sqrt(x**2+y**2+z**2)   # faster than np.linalg.norm
     with np.errstate(divide='ignore', invalid='ignore'):
         # 0/0 produces invalid warn and results in np.nan
         # x/0 produces divide warn and results in np.inf
-        B = (3*np.sum(moment*observer,axis=1)*observer.T/r**5 - moment.T/r**3).T/4/np.pi
+        B = (3*np.sum(moment*observers,axis=1)*observers.T/r**5 - moment.T/r**3).T/4/np.pi
 
     # when r=0 return np.inf in all non-zero moment directions
     mask1 = r==0
diff --git a/magpylib/_src/fields/field_BH_line.py b/magpylib/_src/fields/field_BH_line.py
index ccf2288ef..df9d04301 100644
--- a/magpylib/_src/fields/field_BH_line.py
+++ b/magpylib/_src/fields/field_BH_line.py
@@ -8,10 +8,10 @@
 
 
 def field_BH_line_from_vert(
-    current: np.ndarray,
-    vertex_sets: list,  # list of mix3 ndarrays
-    pos_obs: np.ndarray,
     field: str,
+    observers: np.ndarray,
+    current: np.ndarray,
+    vertices: list,  # list of mix3 ndarrays
     ) -> np.ndarray:
     """
     This function accepts n (mi,3) shaped vertex-sets, creates a single long
@@ -28,27 +28,27 @@ def field_BH_line_from_vert(
     - B-field (ndarray nx3): B-field vectors at pos_obs in units of mT
     """
 
-    nv = len(vertex_sets)           # number of input vertex_sets
-    npp = int(pos_obs.shape[0]/nv)  # number of position vectors
-    nvs = [len(vset)-1 for vset in vertex_sets] # length of vertex sets
+    nv = len(vertices)           # number of input vertex_sets
+    npp = int(observers.shape[0]/nv)  # number of position vectors
+    nvs = [len(vset)-1 for vset in vertices] # length of vertex sets
     nseg = sum(nvs)                             # number of segments
 
     # vertex_sets -> segments
     curr_tile = np.repeat(current, nvs)
-    pos_start = np.concatenate([vert[:-1] for vert in vertex_sets])
-    pos_end = np.concatenate([vert[1:] for vert in vertex_sets])
+    pos_start = np.concatenate([vert[:-1] for vert in vertices])
+    pos_end = np.concatenate([vert[1:] for vert in vertices])
 
     # create input for vectorized computation in one go
-    pos_obs = np.reshape(pos_obs, (nv, npp,3))
-    pos_obs = np.repeat(pos_obs, nvs, axis=0)
-    pos_obs = np.reshape(pos_obs, (-1, 3))
+    observers = np.reshape(observers, (nv, npp,3))
+    observers = np.repeat(observers, nvs, axis=0)
+    observers = np.reshape(observers, (-1, 3))
 
     curr_tile = np.repeat(curr_tile, npp)
     pos_start = np.repeat(pos_start, npp, axis=0)
     pos_end = np.repeat(pos_end, npp, axis=0)
 
     # compute field
-    field = current_line_field(curr_tile, pos_start, pos_end, pos_obs, field=field)
+    field = current_line_field(field, observers, curr_tile, pos_start, pos_end)
     field = np.reshape(field, (nseg, npp, 3))
 
     # sum for each vertex set
@@ -60,11 +60,11 @@ def field_BH_line_from_vert(
 
 # ON INTERFACE
 def current_line_field(
+    field: str,
+    observers: np.ndarray,
     current: np.ndarray,
-    start: np.ndarray,
-    end: np.ndarray,
-    observer: np.ndarray,
-    field='B'
+    segment_start: np.ndarray,
+    segment_end: np.ndarray,
     ) -> np.ndarray:
     """Magnetic field of line current segments.
 
@@ -73,6 +73,13 @@ def current_line_field(
 
     Parameters
     ----------
+    field: str, default=`'B'`
+        If `field='B'` return B-field in units of [mT], if `field='H'` return H-field
+        in units of [kA/m].
+
+    observers: ndarray, shape (n,3)
+        Observer positions (x,y,z) in Cartesian coordinates in units of [mm].
+
     current: ndarray, shape (n,)
         Electrical current in units of [A].
 
@@ -82,13 +89,6 @@ def current_line_field(
     end: ndarray, shape (n,3)
         Line end positions (x,y,z) in Cartesian coordinates in units of [mm].
 
-    observer: ndarray, shape (n,3)
-        Observer positions (x,y,z) in Cartesian coordinates in units of [mm].
-
-    field: str, default=`'B'`
-        If `field='B'` return B-field in units of [mT], if `field='H'` return H-field
-        in units of [kA/m].
-
     Returns
     -------
     B-field or H-field: ndarray, shape (n,3)
@@ -124,7 +124,7 @@ def current_line_field(
     field_all = np.zeros((ntot,3))
 
     # Check for zero-length segments
-    mask0 = np.all(start==end, axis=1)
+    mask0 = np.all(segment_start==segment_end, axis=1)
     if np.all(mask0):
         return field_all
 
@@ -132,12 +132,12 @@ def current_line_field(
     if np.any(mask0):
         not_mask0 = ~mask0     # avoid multiple computation of ~mask
         current = current[not_mask0]
-        start = start[not_mask0]
-        end = end[not_mask0]
-        observer = observer[not_mask0]
+        segment_start = segment_start[not_mask0]
+        segment_end = segment_end[not_mask0]
+        observers = observers[not_mask0]
 
     # rename
-    p1,p2,po = start, end, observer
+    p1,p2,po = segment_start, segment_end, observers
 
     # make dimensionless (avoid all large/small input problems) by introducing
     # the segment length as characteristic length scale.
@@ -150,7 +150,7 @@ def current_line_field(
     t = np.sum((po-p1)*(p1-p2), axis=1)
     p4 = p1 + (t*(p1-p2).T).T
 
-    # distance of observer from line
+    # distance of observers from line
     norm_o4 = norm(po - p4, axis=1)
 
     # separate on-line cases (-> B=0)
diff --git a/magpylib/_src/fields/field_BH_loop.py b/magpylib/_src/fields/field_BH_loop.py
index 79b5ded53..266c26408 100644
--- a/magpylib/_src/fields/field_BH_loop.py
+++ b/magpylib/_src/fields/field_BH_loop.py
@@ -12,10 +12,10 @@
 
 # ON INTERFACE
 def current_loop_field(
+    field: str,
+    observers: np.ndarray,
     current: np.ndarray,
     diameter: np.ndarray,
-    observer: np.ndarray,
-    field='B'
     ) -> np.ndarray:
     """Magnetic field of a circular (line) current loop.
 
@@ -23,19 +23,18 @@ def current_loop_field(
 
     Parameters
     ----------
+    field: str, default=`'B'`
+        If `field='B'` return B-field in units of [mT], if `field='H'` return H-field
+        in units of [kA/m].
+
+    observers: ndarray, shape (n,3)
+        Observer positions (x,y,z) in Cartesian coordinates in units of [mm].
+
     current: ndarray, shape (n,)
         Electrical current in units of [A].
 
     diameter: ndarray, shape (n,)
         Diameter of loop in units of [mm].
-
-    observer: ndarray, shape (n,3)
-        Observer positions (x,y,z) in Cartesian coordinates in units of [mm].
-
-    field: str, default=`'B'`
-        If `field='B'` return B-field in units of [mT], if `field='H'` return H-field
-        in units of [kA/m].
-
     Returns
     -------
     B-field or H-field: ndarray, shape (n,3)
@@ -76,7 +75,7 @@ def current_loop_field(
 
     bh = check_field_input(field, 'current_loop_field()')
 
-    r, phi, z = cart_to_cyl_coordinates(observer)
+    r, phi, z = cart_to_cyl_coordinates(observers)
     rad = np.abs(diameter/2)
     n = len(rad)
 
diff --git a/magpylib/_src/fields/field_BH_sphere.py b/magpylib/_src/fields/field_BH_sphere.py
index 9ee7cf0fa..b24c3be4f 100644
--- a/magpylib/_src/fields/field_BH_sphere.py
+++ b/magpylib/_src/fields/field_BH_sphere.py
@@ -8,10 +8,10 @@
 
 
 def magnet_sphere_field(
+    field: str,
+    observers: np.ndarray,
     magnetization: np.ndarray,
     diameter: np.ndarray,
-    observer: np.ndarray,
-    field='B'
     )->np.ndarray:
     """Magnetic field of a homogeneously magnetized sphere.
 
@@ -19,19 +19,19 @@ def magnet_sphere_field(
 
     Parameters
     ----------
+    field: str, default=`'B'`
+        If `field='B'` return B-field in units of [mT], if `field='H'` return H-field
+        in units of [kA/m].
+
+    observers: ndarray, shape (n,3)
+        Observer positions (x,y,z) in Cartesian coordinates in units of [mm].
+
     magnetization: ndarray, shape (n,3)
         Homogeneous magnetization vector in units of [mT].
 
     diameter: ndarray, shape (n,3)
         Sphere diameter in units of [mm].
 
-    observer: ndarray, shape (n,3)
-        Observer positions (x,y,z) in Cartesian coordinates in units of [mm].
-
-    field: str, default=`'B'`
-        If `field='B'` return B-field in units of [mT], if `field='H'` return H-field
-        in units of [kA/m].
-
     Returns
     -------
     B-field or H-field: ndarray, shape (n,3)
@@ -62,7 +62,7 @@ def magnet_sphere_field(
 
     # all special cases r0=0 and mag=0 automatically covered
 
-    x, y, z = np.copy(observer.T)
+    x, y, z = np.copy(observers.T)
     r = np.sqrt(x**2+y**2+z**2)   # faster than np.linalg.norm
     r0 = abs(diameter)/2
 
@@ -73,7 +73,7 @@ def magnet_sphere_field(
     mask_out = (r>=r0)
 
     mag1 = magnetization[mask_out]
-    obs1 = observer[mask_out]
+    obs1 = observers[mask_out]
     r1 = r[mask_out]
     r01 = r0[mask_out]
 
diff --git a/magpylib/_src/fields/field_wrap_BH_level1.py b/magpylib/_src/fields/field_wrap_BH_level1.py
index 3b1afdc66..265a26f9e 100644
--- a/magpylib/_src/fields/field_wrap_BH_level1.py
+++ b/magpylib/_src/fields/field_wrap_BH_level1.py
@@ -33,7 +33,7 @@ def getBH_level1(**kwargs:dict) -> np.ndarray:
 
     rot = kwargs['orientation']    # only rotation object allowed as input
     pos = kwargs['position']
-    poso = kwargs['observer']
+    poso = kwargs['observers']
 
     # transform obs_pos into source CS
     pos_rel = poso - pos                           # relative position
@@ -43,50 +43,45 @@ def getBH_level1(**kwargs:dict) -> np.ndarray:
     if src_type == 'Cuboid':
         mag = kwargs['magnetization']
         dim = kwargs['dimension']
-        B = magnet_cuboid_field(mag, dim, pos_rel_rot, field=field)
+        B = magnet_cuboid_field(field, pos_rel_rot, mag, dim)
 
     elif src_type == 'Cylinder':
         mag = kwargs['magnetization']
         dim = kwargs['dimension']
-        B = magnet_cylinder_field(mag, dim, pos_rel_rot, field=field)
+        B = magnet_cylinder_field(field, pos_rel_rot, mag, dim)
 
     elif src_type == 'CylinderSegment':
         mag = kwargs['magnetization']
         dim = kwargs['dimension']
-        B = magnet_cylinder_segment_field(mag, dim, pos_rel_rot, field=field)
+        B = magnet_cylinder_segment_field(field, pos_rel_rot, mag, dim)
 
     elif src_type == 'Sphere':
         mag = kwargs['magnetization']
         dia = kwargs['diameter']
-        B = magnet_sphere_field(mag, dia, pos_rel_rot, field=field)
+        B = magnet_sphere_field(field, pos_rel_rot, mag, dia)
 
     elif src_type == 'Dipole':
         moment = kwargs['moment']
-        B = dipole_field(moment, pos_rel_rot, field=field)
+        B = dipole_field(field, pos_rel_rot, moment)
 
     elif src_type == 'Loop':
         current = kwargs['current']
         dia = kwargs['diameter']
-        B = current_loop_field(current, dia, pos_rel_rot, field=field)
+        B = current_loop_field(field, pos_rel_rot, current, dia)
 
     elif src_type =='Line':
         current = kwargs['current']
         if 'vertices' in kwargs:
             vertices = kwargs['vertices']
-            B = field_BH_line_from_vert(current, vertices, pos_rel_rot, field=field)
+            B = field_BH_line_from_vert(field, pos_rel_rot, current, vertices)
         else:
             pos_start = kwargs['segment_start']
             pos_end = kwargs['segment_end']
-            B = current_line_field(current, pos_start, pos_end, pos_rel_rot, field=field)
+            B = current_line_field(field, pos_rel_rot, current, pos_start, pos_end)
 
     elif src_type == 'CustomSource':
-        #bh_key = 'B' if bh else 'H'
-        if kwargs[f'field_{field}_lambda'] is not None:
-            B = kwargs[f'field_{field}_lambda'](pos_rel_rot)
-        else:
-            raise MagpylibInternalError(
-                f'{field}-field calculation not implemented for CustomSource class'
-            )
+        if kwargs.get('field_func', None) is not None:
+            B = kwargs['field_func'](field, pos_rel_rot)
 
     else:
         raise MagpylibInternalError(f'Bad src input type "{src_type}" in level1')
diff --git a/magpylib/_src/fields/field_wrap_BH_level2.py b/magpylib/_src/fields/field_wrap_BH_level2.py
index c24473a6c..54038af45 100644
--- a/magpylib/_src/fields/field_wrap_BH_level2.py
+++ b/magpylib/_src/fields/field_wrap_BH_level2.py
@@ -50,7 +50,7 @@ def get_src_dict(group: list, n_pix: int, n_pp: int, poso: np.ndarray) -> dict:
     kwargs = {
         "source_type": src_type,
         "position": posv,
-        "observer": posov,
+        "observers": posov,
         "orientation": rotobj,
     }
 
@@ -81,12 +81,7 @@ def get_src_dict(group: list, n_pix: int, n_pp: int, poso: np.ndarray) -> dict:
         kwargs.update({"current": currv, "vertices": vert_list})
 
     elif src_type == "CustomSource":
-        kwargs.update(
-            {
-                "field_B_lambda": group[0].field_B_lambda,
-                "field_H_lambda": group[0].field_H_lambda,
-            }
-        )
+        kwargs.update(field_func=group[0].field_func)
 
     else:
         raise MagpylibInternalError("Bad source_type in get_src_dict")
@@ -133,7 +128,7 @@ def getBH_level2(sources, observers, **kwargs) -> np.ndarray:
 
     # CHECK AND FORMAT INPUT ---------------------------------------------------
     if isinstance(sources, str):
-        return getBH_dict_level2(source_type=sources, observer=observers, **kwargs)
+        return getBH_dict_level2(source_type=sources, observers=observers, **kwargs)
 
     # bad user inputs mixing getBH_dict kwargs with object oriented interface
     kwargs_check = kwargs.copy()
@@ -153,10 +148,9 @@ def getBH_level2(sources, observers, **kwargs) -> np.ndarray:
 
     # test if all source dimensions and excitations are initialized
     check_dimensions(sources)
-    check_excitations(sources)
+    check_excitations(sources, kwargs['field'])
 
-
-    # format observer inputs:
+    # format observers input:
     #   allow only bare sensor, collection, pos_vec or list thereof
     #   transform input into an ordered list of sensors (pos_vec->pixel)
     #   check if all pixel shapes are similar - or else if pixel_agg is given
@@ -230,9 +224,7 @@ def getBH_level2(sources, observers, **kwargs) -> np.ndarray:
     groups = {}
     for ind, src in enumerate(src_list):
         if src._object_type == "CustomSource":
-            group_key = (
-                src.field_B_lambda if kwargs["field"] == "B" else src.field_H_lambda
-            )
+            group_key = src.field_func
         else:
             group_key = src._object_type
         if group_key not in groups:
diff --git a/magpylib/_src/fields/field_wrap_BH_level2_dict.py b/magpylib/_src/fields/field_wrap_BH_level2_dict.py
index 064f3f1fe..5a364b3ce 100644
--- a/magpylib/_src/fields/field_wrap_BH_level2_dict.py
+++ b/magpylib/_src/fields/field_wrap_BH_level2_dict.py
@@ -45,8 +45,8 @@ def getBH_dict_level2(**kwargs: dict) -> np.ndarray:
                 f"Input parameter `sources` must be one of {LIBRARY_BH_DICT_SOURCE_STRINGS}"
                 " when using the direct interface.")
 
-        poso = np.array(kwargs['observer'], dtype=float)
-        tile_params['observer'] = (poso,2)    # <-- (input,tdim)
+        poso = np.array(kwargs['observers'], dtype=float)
+        tile_params['observers'] = (poso,2)    # <-- (input,tdim)
 
         # optional general inputs -------------------
         # if no input set pos=0
diff --git a/magpylib/_src/fields/field_wrap_BH_level3.py b/magpylib/_src/fields/field_wrap_BH_level3.py
index c1b9e2cfa..3cc5f0164 100644
--- a/magpylib/_src/fields/field_wrap_BH_level3.py
+++ b/magpylib/_src/fields/field_wrap_BH_level3.py
@@ -39,7 +39,7 @@ def getB(
     pixel_agg: str, default=`None`
         Reference to a compatible numpy aggregator function like `'min'` or `'mean'`,
         which is applied to observer output values, e.g. mean of all sensor pixel outputs.
-        With this option, observer inputs with different (pixel) shapes are allowed.
+        With this option, observers input with different (pixel) shapes is allowed.
 
     Other Parameters (Direct interface)
     -----------------------------------
@@ -112,7 +112,7 @@ def getB(
     [[6.23597388e+00 6.23597388e+00 2.66977810e+00]
      [8.01875374e-01 8.01875374e-01 1.48029737e-16]]
 
-    We can also use sensor objects as observer inputs:
+    We can also use sensor objects as observers input:
 
     >>> sens1 = magpy.Sensor(position=(1,1,1))
     >>> sens2 = sens1.copy(position=(1,1,-1))
@@ -267,7 +267,7 @@ def getH(
     [[4.96243034e+00 4.96243034e+00 2.12454191e+00]
      [6.38112147e-01 6.38112147e-01 1.17798322e-16]]
 
-    We can also use sensor objects as observer inputs:
+    We can also use sensor objects as observers input:
 
     >>> sens1 = magpy.Sensor(position=(1,1,1))
     >>> sens2 = sens1.copy(position=(1,1,-1))
diff --git a/magpylib/_src/input_checks.py b/magpylib/_src/input_checks.py
index 14bacc55e..f2603ff8c 100644
--- a/magpylib/_src/input_checks.py
+++ b/magpylib/_src/input_checks.py
@@ -1,5 +1,6 @@
 """ input checks code"""
 
+import inspect
 import numbers
 import numpy as np
 from scipy.spatial.transform import Rotation
@@ -127,30 +128,45 @@ def check_field_input(inp, origin):
     )
 
 
-def validate_field_lambda(val, bh):
+def validate_field_func(val):
     """test if field function for custom source is valid
     - needs to be a callable
     - input and output shape must match
     """
-    if val is not None:
-        if not callable(val):
-            raise MagpylibBadUserInput(
-                f"Input parameter `field_{bh}_lambda` must be a callable."
-            )
+    if val is None:
+        return None
 
-        out = val(np.array([[1, 2, 3], [4, 5, 6]]))
-        out_shape = np.array(out).shape
-        case2 = out_shape != (2, 3)
+    if not callable(val):
+        raise MagpylibBadUserInput(
+            "Input parameter `field_func` must be a callable.\n"
+            f"Instead received {type(val).__name__}."
+        )
 
-        if case2:
-            raise MagpylibBadUserInput(
-                f"Input parameter `field_{bh}_lambda` must be a callable function"
-                " and return a field ndarray of shape (n,3) when its `observer`"
-                " input is of shape (n,3).\n"
-                f"Instead received shape {out_shape}."
-            )
-    return val
+    fn_args = inspect.getfullargspec(val).args
+    if fn_args[:2] != ["field", "observers"]:
+        raise MagpylibBadUserInput(
+            "Input parameter `field_func` must have two positional args"
+            " called 'field' and 'observers'.\n"
+            f"Instead received a callable where the first two args are: {fn_args[:2]}"
+        )
 
+    for field in ['B', 'H']:
+        out = val(field, np.array([[1, 2, 3], [4, 5, 6]]))
+        if out is not None:
+            if not isinstance(out, np.ndarray):
+                raise MagpylibBadUserInput(
+                    "Input parameter `field_func` must be a callable that returns B- and H-field"
+                    "as numpy ndarray.\n"
+                    f"Instead it returns type {type(out)} for {field}-field."
+                )
+            if out.shape != (2, 3):
+                raise MagpylibBadUserInput(
+                    "Input parameter `field_func` must be a callable that returns B- and H-field"
+                    " as numpy ndarray with shape (n,3), when `observers` input is shape (n,3).\n"
+                    f"Instead it returns shape {out.shape} for {field}-field for input shape (2,3)"
+                )
+
+    return None
 
 #################################################################
 #################################################################
@@ -410,7 +426,7 @@ def check_format_input_backend(inp):
 
 def check_format_input_observers(inp, pixel_agg=None):
     """
-    checks observer input and returns a list of sensor objects
+    checks observers input and returns a list of sensor objects
     """
     # pylint: disable=raise-missing-from
     # pylint: disable=protected-access
@@ -493,7 +509,7 @@ def check_format_input_obj(
     if "sensors" in allow.split("+"):
         wanted_types += list(LIBRARY_SENSORS)
     if "collections" in allow.split("+"):
-        wanted_types += ['Collection']
+        wanted_types += ["Collection"]
 
     if typechecks:
         all_types = list(LIBRARY_SOURCES) + list(LIBRARY_SENSORS) + ["Collection"]
@@ -509,10 +525,7 @@ def check_format_input_obj(
         # recursion
         if (obj_type == "Collection") and recursive:
             obj_list += check_format_input_obj(
-                obj,
-                allow=allow,
-                recursive=recursive,
-                typechecks=typechecks,
+                obj, allow=allow, recursive=recursive, typechecks=typechecks,
             )
 
         # typechecks
@@ -547,7 +560,7 @@ def check_dimensions(sources):
                 raise MagpylibMissingInput(f"Parameter `vertices` of {s} must be set.")
 
 
-def check_excitations(sources):
+def check_excitations(sources, custom_field=None):
     """check if all sources have exitation initialized"""
     # pylint: disable=protected-access
     for s in sources:
@@ -563,6 +576,17 @@ def check_excitations(sources):
         elif obj_type == "Dipole":
             if s.moment is None:
                 raise MagpylibMissingInput(f"Parameter `moment` of {s} must be set.")
+        elif (obj_type == "CustomSource") and (custom_field is not None):
+            if s.field_func is None:
+                raise MagpylibMissingInput(
+                    f"Cannot compute {custom_field}-field because input parameter"
+                    f"`field_func` of {s} has undefined {custom_field}-field computation."
+                )
+            if s.field_func(custom_field, np.zeros((1,3))) is None:
+                raise MagpylibMissingInput(
+                    f"Cannot compute {custom_field}-field because input parameter"
+                    f"`field_func` of {s} has undefined {custom_field}-field computation."
+                )
 
 
 def check_format_pixel_agg(pixel_agg):
diff --git a/magpylib/_src/obj_classes/class_BaseGetBH.py b/magpylib/_src/obj_classes/class_BaseGetBH.py
index d091a0cf9..22423ba4f 100644
--- a/magpylib/_src/obj_classes/class_BaseGetBH.py
+++ b/magpylib/_src/obj_classes/class_BaseGetBH.py
@@ -27,7 +27,7 @@ def getB(self, *observers, squeeze=True, pixel_agg=None):
         pixel_agg: str, default=`None`
             Reference to a compatible numpy aggregator function like `'min'` or `'mean'`,
             which is applied to observer output values, e.g. mean of all sensor pixel outputs.
-            With this option, observer inputs with different (pixel) shapes are allowed.
+            With this option, observers input with different (pixel) shapes is allowed.
 
         Returns
         -------
@@ -88,7 +88,7 @@ def getH(self, *observers, squeeze=True, pixel_agg=None):
         pixel_agg: str, default=`None`
             Reference to a compatible numpy aggregator function like `'min'` or `'mean'`,
             which is applied to observer output values, e.g. mean of all sensor pixel outputs.
-            With this option, observer inputs with different (pixel) shapes are allowed.
+            With this option, observers input with different (pixel) shapes is allowed.
 
         Returns
         -------
diff --git a/magpylib/_src/obj_classes/class_Collection.py b/magpylib/_src/obj_classes/class_Collection.py
index 90f0e4efd..d0b084661 100644
--- a/magpylib/_src/obj_classes/class_Collection.py
+++ b/magpylib/_src/obj_classes/class_Collection.py
@@ -49,11 +49,11 @@ def collection_tree_generator(
     obj,
     format="type+id+label",
     max_elems=20,
-    prefix = "",
-    space = "    ",
-    branch = "│   ",
-    tee = "├── ",
-    last = "└── ",
+    prefix="",
+    space="    ",
+    branch="│   ",
+    tee="├── ",
+    last="└── ",
 ):
     """
     Recursively creates a generator that will yield a visual tree structure of
@@ -74,13 +74,15 @@ def collection_tree_generator(
     if view_props:
         desc = getattr(obj, "_get_description", False)
         if desc:
-            desc_out = desc(exclude=(
-                "children",
-                "parent",
-                "style",
-                "sources",
-                "sensors",
-                "collections")
+            desc_out = desc(
+                exclude=(
+                    "children",
+                    "parent",
+                    "style",
+                    "sources",
+                    "sensors",
+                    "collections",
+                )
             )
             props = [d.strip() for d in desc_out[1:]]
 
@@ -222,7 +224,7 @@ def _repr_html_(self):
             lines.append(line)
         return f"""<pre>{'<br>'.join(lines)}</pre>"""
 
-    def describe(self, format='type+label+id', max_elems=10, return_string=False):
+    def describe(self, format="type+label+id", max_elems=10, return_string=False):
         # pylint: disable=arguments-differ
         """Returns or prints a tree view of the collection.
 
@@ -485,11 +487,11 @@ def _validate_getBH_inputs(self, *inputs):
         return sources, sensors
 
     def getB(self, *inputs, squeeze=True, pixel_agg=None):
-        """Compute B-field in [mT] for given sources and observer inputs.
+        """Compute B-field in [mT] for given sources and observers.
 
         Parameters
         ----------
-        sources_observers: source or observer inputs
+        inputs: source or observer objects
             Input can only be observers if the collection contains only sources. In this case the
             collection behaves like a single source.
             Input can only be sources if the collection contains only sensors. In this case the
@@ -502,7 +504,7 @@ def getB(self, *inputs, squeeze=True, pixel_agg=None):
         pixel_agg: str, default=`None`
             Reference to a compatible numpy aggregator function like `'min'` or `'mean'`,
             which is applied to observer output values, e.g. mean of all sensor pixel outputs.
-            With this option, observer inputs with different (pixel) shapes are allowed.
+            With this option, observers input with different (pixel) shapes is allowed.
 
         Returns
         -------
@@ -547,11 +549,11 @@ def getB(self, *inputs, squeeze=True, pixel_agg=None):
         )
 
     def getH(self, *inputs, squeeze=True, pixel_agg=None):
-        """Compute H-field in [kA/m] for given sources and observer inputs.
+        """Compute H-field in [kA/m] for given sources and observers.
 
         Parameters
         ----------
-        sources_observers: source or observer inputs
+        inputs: source or observer objects
             Input can only be observers if the collection contains only sources. In this case the
             collection behaves like a single source.
             Input can only be sources if the collection contains sensors. In this case the
@@ -564,7 +566,7 @@ def getH(self, *inputs, squeeze=True, pixel_agg=None):
         pixel_agg: str, default=`None`
             Reference to a compatible numpy aggregator function like `'min'` or `'mean'`,
             which is applied to observer output values, e.g. mean of all sensor pixel outputs.
-            With this option, observer inputs with different (pixel) shapes are allowed.
+            With this option, observers input with different (pixel) shapes is allowed.
 
         Returns
         -------
diff --git a/magpylib/_src/obj_classes/class_Sensor.py b/magpylib/_src/obj_classes/class_Sensor.py
index 9e3f19958..206707b05 100644
--- a/magpylib/_src/obj_classes/class_Sensor.py
+++ b/magpylib/_src/obj_classes/class_Sensor.py
@@ -94,7 +94,7 @@ def __init__(
     # property getters and setters
     @property
     def pixel(self):
-        """ Sensor pixel (=sensing elements) positions in the local object coordinates
+        """Sensor pixel (=sensing elements) positions in the local object coordinates
         (rotate with object), in units of [mm].
         """
         return self._pixel
@@ -131,7 +131,7 @@ def getB(self, *sources, sumup=False, squeeze=True, pixel_agg=None):
         pixel_agg: str, default=`None`
             Reference to a compatible numpy aggregator function like `'min'` or `'mean'`,
             which is applied to observer output values, e.g. mean of all sensor pixel outputs.
-            With this option, observer inputs with different (pixel) shapes are allowed.
+            With this option, observers input with different (pixel) shapes is allowed.
 
         Returns
         -------
@@ -192,7 +192,7 @@ def getH(self, *sources, sumup=False, squeeze=True, pixel_agg=None):
         pixel_agg: str, default=`None`
             Reference to a compatible numpy aggregator function like `'min'` or `'mean'`,
             which is applied to observer output values, e.g. mean of all sensor pixel outputs.
-            With this option, observer inputs with different (pixel) shapes are allowed.
+            With this option, observers input with different (pixel) shapes is allowed.
 
         Returns
         -------
diff --git a/magpylib/_src/obj_classes/class_misc_Custom.py b/magpylib/_src/obj_classes/class_misc_Custom.py
index e723b4742..f0cb929be 100644
--- a/magpylib/_src/obj_classes/class_misc_Custom.py
+++ b/magpylib/_src/obj_classes/class_misc_Custom.py
@@ -1,14 +1,11 @@
-"""Custom class code
-DOCSTRINGS V4 READY
-"""
+"""Custom class code """
 
 from magpylib._src.obj_classes.class_BaseGeo import BaseGeo
 from magpylib._src.obj_classes.class_BaseDisplayRepr import BaseDisplayRepr
 from magpylib._src.obj_classes.class_BaseGetBH import BaseGetBH
-from magpylib._src.input_checks import validate_field_lambda
+from magpylib._src.input_checks import validate_field_func
 
 
-# ON INTERFACE
 class CustomSource(BaseGeo, BaseDisplayRepr, BaseGetBH):
     """User-defined custom source.
 
@@ -19,13 +16,12 @@ class CustomSource(BaseGeo, BaseDisplayRepr, BaseGetBH):
 
     Parameters
     ----------
-    field_B_lambda: callable, default=`None`
-        Field function for the B-field. Must accept position input with format (n,3) and
-        return the B-field with similar shape in units of [mT].
-
-    field_H_lambda: callable, default=`None`
-        Field function for the H-field. Must accept position input with format (n,3) and
-        return the H-field with similar shape in units of [kA/m].
+    field_func: callable, default=`None`
+        The function for B- and H-field computation must have the two positional arguments
+        `field` and `observers`. With `field='B'` or `field='H'` the B- or H-field in units
+        of [mT] or [kA/m] must be returned respectively. The `observers` argument must
+        accept numpy ndarray inputs of shape (n,3), in which case the returned fields must
+        be numpy ndarrays of shape (n,3) themselves.
 
     position: array_like, shape (3,) or (m,3), default=`(0,0,0)`
         Object position(s) in the global coordinates in units of [mm]. For m>1, the
@@ -57,9 +53,8 @@ class CustomSource(BaseGeo, BaseDisplayRepr, BaseGetBH):
     >>> import numpy as np
     >>> import magpylib as magpy
     >>>
-    >>> bfield = lambda observer: np.array([(100,0,0)]*len(observer))
-    >>> hfield = lambda observer: np.array([(80,0,0)]*len(observer))
-    >>> src = magpy.misc.CustomSource(field_B_lambda=bfield, field_H_lambda=hfield)
+    >>> funcBH = lambda field, obs: np.array([(100 if field=='B' else 80,0,0)]*len(obs))
+    >>> src = magpy.misc.CustomSource(field_func=funcBH)
     >>> H = src.getH((1,1,1))
     >>> print(H)
     [80.  0.  0.]
@@ -87,16 +82,14 @@ class CustomSource(BaseGeo, BaseDisplayRepr, BaseGetBH):
 
     def __init__(
         self,
-        field_B_lambda=None,
-        field_H_lambda=None,
+        field_func=None,
         position=(0, 0, 0),
         orientation=None,
         style=None,
         **kwargs,
     ):
         # instance attributes
-        self.field_B_lambda = field_B_lambda
-        self.field_H_lambda = field_H_lambda
+        self.field_func = field_func
         self._object_type = "CustomSource"
 
         # init inheritance
@@ -104,25 +97,17 @@ def __init__(
         BaseDisplayRepr.__init__(self)
 
     @property
-    def field_B_lambda(self):
-        """
-        Field function for the B-field. Must accept position input with format (n,3) and
-        return the B-field with similar shape in units of [mT].
-        """
-        return self._field_B_lambda
-
-    @field_B_lambda.setter
-    def field_B_lambda(self, val):
-        self._field_B_lambda = validate_field_lambda(val, "B")
-
-    @property
-    def field_H_lambda(self):
+    def field_func(self):
         """
-        Field function for the H-field. Must accept position input with format (n,3) and
-        return the H-field with similar shape in units of [kA/m].
+        The function for B- and H-field computation must have the two positional arguments
+        `field` and `observers`. With `field='B'` or `field='H'` the B- or H-field in units
+        of [mT] or [kA/m] must be returned respectively. The `observers` argument must
+        accept numpy ndarray inputs of shape (n,3), in which case the returned fields must
+        be numpy ndarrays of shape (n,3) themselves.
         """
-        return self._field_H_lambda
+        return self._field_func
 
-    @field_H_lambda.setter
-    def field_H_lambda(self, val):
-        self._field_H_lambda = validate_field_lambda(val, "H")
+    @field_func.setter
+    def field_func(self, val):
+        validate_field_func(val)
+        self._field_func = val


Test patch:
diff --git a/tests/test_CustomSource.py b/tests/test_CustomSource.py
index 07121dd23..ad8622132 100644
--- a/tests/test_CustomSource.py
+++ b/tests/test_CustomSource.py
@@ -1,31 +1,19 @@
-import pytest
 import numpy as np
 import magpylib as magpy
-from magpylib._src.exceptions import MagpylibBadUserInput, MagpylibInternalError
 
 # pylint: disable=assignment-from-no-return
 # pylint: disable=unused-argument
 
-def constant_Bfield(position=(0,0,0)):
+def constant_field(field, observers=(0,0,0)):
     """ constant field"""
-    position = np.array(position)
+    position = np.array(observers)
     length = 1 if position.ndim==1 else len(position)
     return np.array([[1, 2, 3]] * length)
 
-def constant_Hfield(position=(0, 0, 0)):
-    """ constant field - no idea why we need this """
-    position = np.array(position)
-    length = 1 if position.ndim==1 else len(position)
-    return np.array([[1, 2, 3]] * length)
-
-def bad_Bfield_func(position):
-    """ another constant function without docstring"""
-    return np.array([[1, 2, 3]])
-
 
 def test_CustomSource_basicB():
     """Basic custom source class test"""
-    external_field = magpy.misc.CustomSource(field_B_lambda=constant_Bfield)
+    external_field = magpy.misc.CustomSource(field_func=constant_field)
 
     B = external_field.getB((1, 2, 3))
     Btest = np.array((1, 2, 3))
@@ -43,7 +31,7 @@ def test_CustomSource_basicB():
 
 def test_CustomSource_basicH():
     """Basic custom source class test"""
-    external_field = magpy.misc.CustomSource(field_H_lambda=constant_Hfield)
+    external_field = magpy.misc.CustomSource(field_func=constant_field)
 
     H = external_field.getH((1, 2, 3))
     Htest = np.array((1, 2, 3))
@@ -59,19 +47,6 @@ def test_CustomSource_basicH():
     np.testing.assert_allclose(H, Htest)
 
 
-def test_CustomSource_bad_inputs():
-    """missing docstring"""
-    with pytest.raises(MagpylibBadUserInput):
-        magpy.misc.CustomSource(field_H_lambda='not a callable')
-
-    with pytest.raises(MagpylibBadUserInput):
-        magpy.misc.CustomSource(field_H_lambda=bad_Bfield_func)
-
-    src = magpy.misc.CustomSource()
-    with pytest.raises(MagpylibInternalError):
-        src.getB([0,0,0])
-
-
 def test_repr():
     """test __repr__"""
     dip = magpy.misc.CustomSource()
diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py
index dbae4fddf..5a0f9d302 100644
--- a/tests/test_exceptions.py
+++ b/tests/test_exceptions.py
@@ -17,7 +17,7 @@ def getBHv_unknown_source_type():
         magnetization=(1,0,0),
         dimension=(0,2,1,0,360),
         position=(0,0,-.5),
-        observer=(1.5,0,-.1),
+        observers=(1.5,0,-.1),
         field='B')
 
 def getBH_level1_internal_error():
@@ -25,7 +25,7 @@ def getBH_level1_internal_error():
     """
     x = np.array([(1,2,3)])
     rot = R.from_quat((0,0,0,1))
-    getBH_level1(field='B',source_type='woot', magnetization=x, dimension=x, observer=x,
+    getBH_level1(field='B',source_type='woot', magnetization=x, dimension=x, observers=x,
         position=x, orientation=rot)
 
 
@@ -62,18 +62,18 @@ def getBHv_missing_input1():
     """ missing bh
     """
     x=np.array([(1,2,3)])
-    getBH_dict_level2(source_type='Cuboid', observer=x, magnetization=x, dimension=x)
+    getBH_dict_level2(source_type='Cuboid', observers=x, magnetization=x, dimension=x)
 
 
 def getBHv_missing_input2():
     """ missing source_type
     """
     x=np.array([(1,2,3)])
-    getBH_dict_level2(bh=True, observer=x, magnetization=x, dimension=x)
+    getBH_dict_level2(bh=True, observers=x, magnetization=x, dimension=x)
 
 
 def getBHv_missing_input3():
-    """ missing observer
+    """ missing observers
     """
     x=np.array([(1,2,3)])
     getBH_dict_level2(bh=True, source_type='Cuboid', magnetization=x, dimension=x)
@@ -83,14 +83,14 @@ def getBHv_missing_input4_cuboid():
     """ missing Cuboid mag
     """
     x=np.array([(1,2,3)])
-    getBH_dict_level2(bh=True, source_type='Cuboid', observer=x, dimension=x)
+    getBH_dict_level2(bh=True, source_type='Cuboid', observers=x, dimension=x)
 
 
 def getBHv_missing_input5_cuboid():
     """ missing Cuboid dim
     """
     x=np.array([(1,2,3)])
-    getBH_dict_level2(bh=True, source_type='Cuboid', observer=x, magnetization=x)
+    getBH_dict_level2(bh=True, source_type='Cuboid', observers=x, magnetization=x)
 
 
 def getBHv_missing_input4_cyl():
@@ -98,28 +98,28 @@ def getBHv_missing_input4_cyl():
     """
     x=np.array([(1,2,3)])
     y = np.array([(1,2)])
-    getBH_dict_level2(bh=True, source_type='Cylinder', observer=x, dimension=y)
+    getBH_dict_level2(bh=True, source_type='Cylinder', observers=x, dimension=y)
 
 
 def getBHv_missing_input5_cyl():
     """ missing Cylinder dim
     """
     x=np.array([(1,2,3)])
-    getBH_dict_level2(bh=True, source_type='Cylinder', observer=x, magnetization=x)
+    getBH_dict_level2(bh=True, source_type='Cylinder', observers=x, magnetization=x)
 
 
 def getBHv_missing_input4_sphere():
     """ missing Sphere mag
     """
     x=np.array([(1,2,3)])
-    getBH_dict_level2(bh=True, source_type='Sphere', observer=x, dimension=1)
+    getBH_dict_level2(bh=True, source_type='Sphere', observers=x, dimension=1)
 
 
 def getBHv_missing_input5_sphere():
     """ missing Sphere dim
     """
     x=np.array([(1,2,3)])
-    getBH_dict_level2(bh=True, source_type='Sphere', observer=x, magnetization=x)
+    getBH_dict_level2(bh=True, source_type='Sphere', observers=x, magnetization=x)
 
 # bad inputs -------------------------------------------------------------------
 def getBHv_bad_input1():
@@ -127,14 +127,14 @@ def getBHv_bad_input1():
     """
     x=np.array([(1,2,3)])
     x2=np.array([(1,2,3)]*2)
-    getBH_dict_level2(bh=True, source_type='Cuboid', observer=x, magnetization=x2, dimension=x)
+    getBH_dict_level2(bh=True, source_type='Cuboid', observers=x, magnetization=x2, dimension=x)
 
 
 def getBHv_bad_input2():
     """ bad source_type string
     """
     x=np.array([(1,2,3)])
-    getBH_dict_level2(bh=True, source_type='Cubooid', observer=x, magnetization=x, dimension=x)
+    getBH_dict_level2(bh=True, source_type='Cubooid', observers=x, magnetization=x, dimension=x)
 
 
 def getBHv_bad_input3():
@@ -142,7 +142,7 @@ def getBHv_bad_input3():
     """
     x=np.array([(1,2,3)])
     s = magpy.Sensor()
-    getBH_dict_level2(bh=True, source_type='Cuboid', observer=s, magnetization=x, dimension=x)
+    getBH_dict_level2(bh=True, source_type='Cuboid', observers=s, magnetization=x, dimension=x)
 
 
 def utility_format_obj_input():
diff --git a/tests/test_field_cylinder.py b/tests/test_field_cylinder.py
index c850f839a..9f5220102 100644
--- a/tests/test_field_cylinder.py
+++ b/tests/test_field_cylinder.py
@@ -220,7 +220,7 @@ def test_cylinder_field1():
     eins = np.ones(N)
     d, h, _ = dim.T
     dim5 = np.array([nulll, d / 2, h, nulll, eins * 360]).T
-    B1 = magnet_cylinder_segment_field(magg, dim5, poso)
+    B1 = magnet_cylinder_segment_field('B', poso, magg, dim5)
 
     assert np.allclose(B1, B0)
 
@@ -450,10 +450,10 @@ def test_cylinder_diametral_small_r():
     test if the gneral case fluctuations are small
     """
     B = magpy.core.magnet_cylinder_field(
-        np.array([(1,1,0)]*1000),
-        np.array([(2,2)]*1000),
+        'B',
         np.array([(x,0,3) for x in np.logspace(-1.4,-1.2,1000)]),
-        field='B')
+        np.array([(1,1,0)]*1000),
+        np.array([(2,2)]*1000))
 
     dB = np.log(abs(B[1:]-B[:-1]))
     ddB = abs(dB[1:]-dB[:-1])
diff --git a/tests/test_field_functions.py b/tests/test_field_functions.py
index f02d06f6c..78340db52 100644
--- a/tests/test_field_functions.py
+++ b/tests/test_field_functions.py
@@ -14,7 +14,7 @@ def test_magnet_cuboid_Bfield():
     mag = np.array([(0,0,0), (1,2,3), (1,2,3), (1,2,3), (1,2,3), (2,2,2), (2,2,2), (1,1,1), (1,1,1)])
     dim = np.array([(1,2,3), (-1,-2,2), (1,2,2), (0,2,2), (1,2,3), (2,2,2), (2,2,2), (2,2,2), (2,2,2)])
     pos = np.array([(1,2,3), (1,-1,0), (1,-1,0), (1,-1,0), (1,2,3), (1,1+1e-14,0), (1,1,1), (1,-1,2), (1+1e-14,-1,2)])
-    B = magnet_cuboid_field(mag, dim, pos)
+    B = magnet_cuboid_field('B', pos, mag, dim)
 
     Btest = [
     [ 0.         , 0.         , 0.        ],
@@ -37,7 +37,7 @@ def test_magnet_cuboid_field_mag0():
     mag = np.zeros((n,3))
     dim = np.random.rand(n,3)
     pos = np.random.rand(n,3)
-    B = magnet_cuboid_field(mag, dim, pos)
+    B = magnet_cuboid_field('B', pos, mag, dim)
     assert_allclose(mag,B)
 
 
@@ -51,7 +51,7 @@ def test_field_BH_cylinder_tile_mag0():
     phi2=phi1+phi2
     dim = np.array([r1,r2,h,phi1,phi2]).T
     pos = np.random.rand(n,3)
-    B = magnet_cylinder_segment_field(mag, dim, pos)
+    B = magnet_cylinder_segment_field('B', pos, mag, dim)
     assert_allclose(mag, B)
 
 
@@ -71,7 +71,7 @@ def test_field_sphere_vs_v2():
     dim = np.array([1.23]*7)
     mag = np.array([(33,66,99)]*7)
     poso = np.array([(0,0,0),(.2,.2,.2),(.4,.4,.4),(-1,-1,-2),(.1,.1,.1),(1,2,-3),(-3,2,1)])
-    B = magnet_sphere_field(mag, dim, poso )
+    B = magnet_sphere_field('B', poso, mag, dim)
 
     np.testing.assert_allclose(result_v2, B, rtol=1e-6)
 
@@ -83,7 +83,7 @@ def test_magnet_sphere_field_mag0():
     mag = np.zeros((n,3))
     dim = np.random.rand(n)
     pos = np.random.rand(n,3)
-    B = magnet_sphere_field(mag, dim, pos)
+    B = magnet_sphere_field('B', pos, mag, dim)
     assert_allclose(mag,B)
 
 
@@ -92,7 +92,7 @@ def test_field_dipole1():
     """
     poso = np.array([(1,2,3),(-1,2,3)])
     mom = np.array([(2,3,4),(0,-3,-2)])
-    B = dipole_field(mom, poso, field='B')*np.pi
+    B = dipole_field('B', poso, mom)*np.pi
     Btest = np.array([
         (0.01090862,0.02658977,0.04227091),
         (0.0122722,-0.01022683,-0.02727156),
@@ -106,7 +106,7 @@ def test_field_dipole2():
     """
     moment = np.array([(100,200,300)]*2 + [(0,0,0)]*2)
     observer = np.array([(0,0,0),(1,2,3)]*2)
-    B = dipole_field(moment, observer)
+    B = dipole_field('B', observer, moment)
 
     assert all(np.isinf(B[0]))
     assert_allclose(B[1:],
@@ -145,12 +145,12 @@ def test_field_loop():
     current = np.array([1,1] + [123]*8)
     dim = np.array([2,2] + [2]*8)
 
-    B = current_loop_field(current, dim, pos_test)
+    B = current_loop_field('B', pos_test, current, dim)
 
     assert_allclose(B, Btest, rtol=1e-6)
 
     Htest = Btest*10/4/np.pi
-    H = current_loop_field(current, dim, pos_test, field='H')
+    H = current_loop_field('H', pos_test, current, dim)
     assert_allclose(H, Htest, rtol=1e-6)
 
 
@@ -160,12 +160,12 @@ def test_field_loop2():
     curr = np.array([1])
     dim = np.array([2])
     poso = np.array([[0,0,0]])
-    B = current_loop_field(curr, dim, poso)
+    B = current_loop_field('B', poso, curr, dim)
 
     curr = np.array([1]*2)
     dim = np.array([2]*2)
     poso = np.array([[0,0,0]]*2)
-    B2 = current_loop_field(curr, dim, poso)
+    B2 = current_loop_field('B', poso, curr, dim)
 
     assert_allclose(B, (B2[0],))
     assert_allclose(B, (B2[1],))
@@ -178,7 +178,7 @@ def test_field_loop_specials():
     dia = np.array([2,2,0,0,2,2])
     obs = np.array([(0,0,0), (1,0,0), (0,0,0), (1,0,0), (1,0,0), (0,0,0)])
 
-    B = current_loop_field(cur, dia, obs)
+    B = current_loop_field('B', obs, cur, dia)
     Btest = [[0,0,0.62831853], [0,0,0], [0,0,0], [0,0,0], [0,0,0], [0,0,1.25663706]]
     assert_allclose(B, Btest)
 
@@ -193,18 +193,18 @@ def test_field_line():
     pe1 = np.array([(2,2,2)])
 
     # only normal
-    B1 = current_line_field(c1, ps1, pe1, po1)
+    B1 = current_line_field('B', po1, c1, ps1, pe1)
     x1 = np.array([[ 0.02672612, -0.05345225, 0.02672612]])
     assert_allclose(x1, B1, rtol=1e-6)
 
     # only on_line
     po1b = np.array([(1,1,1)])
-    B2 = current_line_field(c1, ps1, pe1, po1b)
+    B2 = current_line_field('B', po1b, c1, ps1, pe1)
     x2 = np.zeros((1,3))
     assert_allclose(x2, B2, rtol=1e-6)
 
     # only zero-segment
-    B3 = current_line_field(c1, ps1, ps1, po1)
+    B3 = current_line_field('B', po1, c1, ps1, ps1)
     x3 = np.zeros((1,3))
     assert_allclose(x3, B3, rtol=1e-6)
 
@@ -213,19 +213,19 @@ def test_field_line():
     ps2 = np.array([(0,0,0)]*2)
     pe2 = np.array([(0,0,0),(2,2,2)])
     po2 = np.array([(1,2,3),(1,1,1)])
-    B4 = current_line_field(c2, ps2, pe2, po2)
+    B4 = current_line_field('B', po2, c2, ps2, pe2)
     x4 = np.zeros((2,3))
     assert_allclose(x4, B4, rtol=1e-6)
 
     # normal + zero_segment
     po2b = np.array([(1,2,3),(1,2,3)])
-    B5 = current_line_field(c2, ps2, pe2, po2b)
+    B5 = current_line_field('B', po2b, c2, ps2, pe2)
     x5 = np.array([[0,0,0],[ 0.02672612, -0.05345225, 0.02672612]])
     assert_allclose(x5, B5, rtol=1e-6)
 
     # normal + on_line
     pe2b = np.array([(2,2,2)]*2)
-    B6 = current_line_field(c2, ps2, pe2b, po2)
+    B6 = current_line_field('B', po2, c2, ps2, pe2b)
     x6 = np.array([[0.02672612, -0.05345225, 0.02672612],[0,0,0]])
     assert_allclose(x6, B6, rtol=1e-6)
 
@@ -234,7 +234,7 @@ def test_field_line():
     ps4 = np.array([(0,0,0)]*3)
     pe4 = np.array([(0,0,0),(2,2,2),(2,2,2)])
     po4 = np.array([(1,2,3),(1,2,3),(1,1,1)])
-    B7 = current_line_field(c4, ps4, pe4, po4)
+    B7 = current_line_field('B', po4, c4, ps4, pe4)
     x7 = np.array([[0,0,0], [0.02672612, -0.05345225, 0.02672612], [0,0,0]])
     assert_allclose(x7, B7, rtol=1e-6)
 
@@ -250,7 +250,7 @@ def test_field_line_from_vert():
     vert3 = np.array([(1,2,3),(-2,-3,3),(3,2,1),(3,3,3)])
 
     pos_tiled = np.tile(p, (3,1))
-    B_vert = field_BH_line_from_vert(curr, [vert1,vert2,vert3], pos_tiled, field='B')
+    B_vert = field_BH_line_from_vert('B', pos_tiled, curr, [vert1,vert2,vert3])
 
     B = []
     for i,vert in enumerate([vert1,vert2,vert3]):
@@ -259,7 +259,7 @@ def test_field_line_from_vert():
             p2 = vert[1:]
             po = np.array([pos]*(len(vert)-1))
             cu = np.array([curr[i]]*(len(vert)-1))
-            B += [np.sum(current_line_field(cu, p1, p2, po), axis=0)]
+            B += [np.sum(current_line_field('B', po, cu, p1, p2), axis=0)]
     B = np.array(B)
 
     assert_allclose(B_vert, B)
@@ -272,7 +272,7 @@ def test_field_line_v4():
     start = np.array([(-1,0,0)]*7)
     end = np.array([(1,0,0), (-1,0,0), (1,0,0), (-1,0,0)] + [(1,0,0)]*3)
     obs = np.array([(0,0,1),(0,0,0), (0,0,0), (0,0,0), (0,0,1e-16), (2,0,1), (-2,0,1)])
-    B = current_line_field(cur, start, end, obs)
+    B = current_line_field('B', obs, cur, start, end)
     Btest = np.array(
         [[0, -0.14142136, 0],
          [0,  0.        , 0],
diff --git a/tests/test_getBH_level2.py b/tests/test_getBH_level2.py
index 0517a29c3..bdadf2c13 100644
--- a/tests/test_getBH_level2.py
+++ b/tests/test_getBH_level2.py
@@ -298,7 +298,6 @@ def test_squeeze_sumup():
 
     assert B1.shape == B2.shape
 
-
 def test_pixel_agg():
     """test pixel aggregator"""
     src1 = magpy.magnet.Cuboid((0,0,1000),(1,1,1)).move([[1,0,0]])
diff --git a/tests/test_input_checks.py b/tests/test_input_checks.py
index edab887dd..4fb08ecf6 100644
--- a/tests/test_input_checks.py
+++ b/tests/test_input_checks.py
@@ -1,8 +1,12 @@
 import numpy as np
 from scipy.spatial.transform import Rotation as R
-from magpylib._src.exceptions import MagpylibBadUserInput, MagpylibMissingInput
+from magpylib._src.exceptions import (
+    MagpylibBadUserInput,
+    MagpylibMissingInput,
+)
 import magpylib as magpy
 
+
 ###########################################################
 ###########################################################
 # OBJECT INPUTS
@@ -378,25 +382,90 @@ def test_input_objects_dimension_cylinderSegment_bad():
             magpy.magnet.CylinderSegment(dimension=bad)
 
 
-def test_input_objects_fiedBHlambda_good():
-    """good input: magpy.misc.CustomSource(field_B_lambda=f, field_H_lambda=f)"""
-    def f(x):
-        """3 in 3 out"""
-        return x
-    src = magpy.misc.CustomSource(field_B_lambda=f, field_H_lambda=f)
+def test_input_objects_field_func_good():
+    """good input: magpy.misc.CustomSource(field_func=f)"""
+    # pylint: disable=unused-argument
+
+    # init empty = None
+    src = magpy.misc.CustomSource()
+    np.testing.assert_raises(MagpylibMissingInput, src.getB, (1,2,3))
+    np.testing.assert_raises(MagpylibMissingInput, src.getH, (1,2,3))
 
+    # None
+    src = magpy.misc.CustomSource(field_func=None)
+    np.testing.assert_raises(MagpylibMissingInput, src.getB, (1,2,3))
+    np.testing.assert_raises(MagpylibMissingInput, src.getH, (1,2,3))
+
+    # acceptable func with B and H return
+    def f(field, observers):
+        """3 in 3 out"""
+        return observers
+    src = magpy.misc.CustomSource(field_func=f)
     np.testing.assert_allclose(src.getB((1,2,3)), (1,2,3))
     np.testing.assert_allclose(src.getH((1,2,3)), (1,2,3))
 
+    # acceptable func with only B return
+    def ff(field, observers):
+        """3 in 3 out"""
+        if field == 'B':
+            return observers
+        return None
+    src = magpy.misc.CustomSource(field_func=ff)
+    np.testing.assert_allclose(src.getB((1,2,3)), (1,2,3))
+    np.testing.assert_raises(MagpylibMissingInput, src.getH, (1,2,3))
+
+    # acceptable func with only B return
+    def fff(field, observers):
+        """3 in 3 out"""
+        if field == 'H':
+            return observers
+        return None
+    src = magpy.misc.CustomSource(field_func=fff)
+    np.testing.assert_raises(MagpylibMissingInput, src.getB, (1,2,3))
+    np.testing.assert_allclose(src.getH((1,2,3)), (1,2,3))
 
-def test_input_objects_fiedBHlambda_bad():
-    """bad input: magpy.misc.CustomSource(field_B_lambda=f, field_H_lambda=f)"""
-    def f(x):
-        """bad fieldBH lambda"""
-        return 1
-    np.testing.assert_raises(MagpylibBadUserInput, magpy.misc.CustomSource, f)
-    np.testing.assert_raises(MagpylibBadUserInput, magpy.misc.CustomSource, field_H_lambda=f)
 
+def test_input_objects_field_func_bad():
+    """bad input: magpy.misc.CustomSource(field_func=f)"""
+    # pylint: disable=unused-argument
+
+    # non callable
+    np.testing.assert_raises(MagpylibBadUserInput, magpy.misc.CustomSource, 1)
+
+    # bad arg names
+    def ff(fieldd, observers, whatever):
+        """ ff """
+    np.testing.assert_raises(MagpylibBadUserInput, magpy.misc.CustomSource, ff)
+
+    # no ndarray return on B
+    def fff(field, observers):
+        """ fff """
+        if field == 'B':
+            return 1
+    np.testing.assert_raises(MagpylibBadUserInput, magpy.misc.CustomSource, fff)
+
+    # no ndarray return on H
+    def ffff(field, observers):
+        """ ffff """
+        if field == 'H':
+            return 1
+        return observers
+    np.testing.assert_raises(MagpylibBadUserInput, magpy.misc.CustomSource, ffff)
+
+    # bad return shape on B
+    def g(field, observers):
+        """ g """
+        if field == 'B':
+            return np.array([1,2,3])
+    np.testing.assert_raises(MagpylibBadUserInput, magpy.misc.CustomSource, g)
+
+    # bad return shape on H
+    def gg(field, observers):
+        """ gg """
+        if field == 'H':
+            return np.array([1,2,3])
+        return observers
+    np.testing.assert_raises(MagpylibBadUserInput, magpy.misc.CustomSource, gg)
 
 
 ###########################################################
@@ -593,8 +662,8 @@ def test_input_rotate_axis_bad():
         np.testing.assert_raises(MagpylibBadUserInput, x.rotate_from_angax, 10, bad)
 
 
-def test_input_observer_good():
-    """good observer inputs"""
+def test_input_observers_good():
+    """good observers input"""
     pos_vec1 = (1,2,3)
     pos_vec2 = [(1,2,3)]*2
     pos_vec3 = [[(1,2,3)]*2]*3
@@ -621,8 +690,8 @@ def test_input_observer_good():
         assert isinstance(B, np.ndarray)
 
 
-def test_input_observer_bad():
-    """bad observer inputs"""
+def test_input_observers_bad():
+    """bad observers input"""
     pos_vec1 = (1,2,3)
     pos_vec2 = [(1,2,3)]*2
     sens1 = magpy.Sensor()
@@ -814,7 +883,7 @@ def test_input_getBH_field_good():
     for good in goods:
         moms = np.array([[1,2,3]])
         obs = np.array([[1,2,3]])
-        B = magpy.core.dipole_field(moms, obs, field=good)
+        B = magpy.core.dipole_field(good, obs, moms)
         assert isinstance(B, np.ndarray)
 
 
@@ -838,7 +907,7 @@ def test_input_getBH_field_bad():
         np.testing.assert_raises(
             MagpylibBadUserInput,
             magpy.core.dipole_field,
-            moms,
+            bad,
             obs,
-            field=bad,
+            moms,
         )
diff --git a/tests/test_obj_Cuboid.py b/tests/test_obj_Cuboid.py
index 9d8b0c807..263535d29 100644
--- a/tests/test_obj_Cuboid.py
+++ b/tests/test_obj_Cuboid.py
@@ -98,8 +98,8 @@ def test_cuboid_object_vs_lib():
     mag = np.array([(10,20,30)])
     dim = np.array([(  a,  a,  a)])
     pos = np.array([(2*a,2*a,2*a)])
-    B0 = magpy.core.magnet_cuboid_field(mag, dim, pos)
-    H0 = magpy.core.magnet_cuboid_field(mag, dim, pos, field='H')
+    B0 = magpy.core.magnet_cuboid_field('B', pos, mag, dim)
+    H0 = magpy.core.magnet_cuboid_field('H', pos, mag, dim)
 
     src = magpy.magnet.Cuboid(mag[0], dim[0])
     B1 = src.getB(pos)
diff --git a/tests/test_obj_Sphere.py b/tests/test_obj_Sphere.py
index 82dd6f5fe..cd60f35e3 100644
--- a/tests/test_obj_Sphere.py
+++ b/tests/test_obj_Sphere.py
@@ -94,7 +94,7 @@ def test_sphere_object_vs_lib():
     mag = np.array([(10,20,30)])
     dia = np.array([1])
     pos = np.array([(2,2,2)])
-    B1 = magpy.core.magnet_sphere_field(mag, dia, pos)[0]
+    B1 = magpy.core.magnet_sphere_field('B', pos, mag, dia)[0]
 
     src = magpy.magnet.Sphere(mag[0], dia[0])
     B2 = src.getB(pos)