human
I am working on the file "Analytical Helmholtz.ipynb".
The first task is:
Class Definition
Here, we build a class called AnalyticalHelmholtz, which inherits from object.
The __init__ method is called automatically when we instantiate the class (i.e., make a new object based on AnalyticalHelmholtz). The __init__ method expects one or two inputs: 1) a required input (dictionary) called sc, which contains the configuration for the system, and 2) an optional input called hf, which defaults to the hankel function from scipy.special.
When running __init__, Python first stores some properties from the sc parameter, and determines whether it is going to run in 2D mode or 3D mode. At the end of this method, a member object _hf is created, which stores a reference to the chosen Hankel function (from hf).
When an object of class AnalyticalHelmholtz is called like a function, Python executes the __call__ method. This uses whichever Green's function was set up during __init__, and returns a wavefield. The distance from the source is calculated for each point in the grid (from the $x$ and $z$ locations passed into the function call), and the response is returned as a numpy array (based on the configuration in the sc object from when the AnalyticalHelmholtz class was instantiated).
NB: k is a property, which behaves like a variable bound to the instance, but actually runs some code inside the k method when its value is asked for. This way, it can be calculated and stored based on the omega and c properties.
Can you write Python code for it?
assistant
#Here is the Python code you requested.

class AnalyticalHelmholtz(object):
    '''
    Implements the analytical Green's functions for the 2D and 3D
    Helmholtz equation(s).
    ''' 

    def __init__(self, sc, hf = hankel1):

        self.dimension = sc.get('dimension', 2)
        self.omega = sc['omega']
        self.c = sc['c']

        if self.dimension == 3:
            self.Green = self.Green3D
        else:
            self.Green = self.Green2D

        self._x, self._z = np.mgrid[
            sc['xorig']:sc['xorig']+sc['dx']*sc['nx']:sc['dx'],
            sc['zorig']:sc['zorig']+sc['dz']*sc['nz']:sc['dz']
        ]
        
        self._hf = hf

    def Green2D(self, x):

        return -0.5j * self._hf(0, self.k*x)

    def Green3D(self, x):

        return (1./(4*np.pi*x)) * np.exp(1j*self.k*x)

    def __call__(self, x, z):

        return np.nan_to_num(self.Green(np.sqrt((x - self._x)**2 + (z - self._z)**2)))
    
    @property
    def k(self):
        if getattr(self, '_k', None) is None:
            self._k = self.omega / self.c
        return self._k