Add Gaussian quadrature integration for line shape models#378
Conversation
…ine to use numerical integration over the spectral bin.
Mateasek
left a comment
There was a problem hiding this comment.
Hi @vsnever , thank you for bringing this PR. I really like it and I think it is a great idea. I added some minor comments to the code, but I have one conceptual idea.
For every lineshape, we have to have the integral value of the power within the bin. For some line shapes there exists an analytical function, some have to be done numerically. Also definite integrals are functions of 2 variables. What if you made Function2D base class for the Integrator1D and then instead of having LineShapeModel and NumericallyIntegrableLineShapeModel classes, there could be again only one class, which would take a Function2D instance as the integrator parameter? In the code it does not matter really, if you are calling an analytically defined Function2D (created with the function framework) of if you call a numerical integrator, also being a Function2D.
This would also need to store the func parameter from integrate method as a attribute of the Integrator1D class, but I think that it is only a minor change.
I hope I managed to express myself clearly. What do you think?
|
|
||
| i = 0 | ||
| for order in range(self._min_order, self._max_order + 1): | ||
| self._roots[i:i + order], self._weights[i:i + order] = roots_legendre(order) |
There was a problem hiding this comment.
in the integrate method you do operation (self._roots_mv[i] + 1.), storing root + 1 value in the array could bring some speedups and it seems that you don't use the value of root elsewhere
There was a problem hiding this comment.
I rewrote the integration, so x now taken as x = c + d * self._roots_mv[i], where c = 0.5 * (a + b) and d = 0.5 * (b - a). Adding 1 is no longer required.
| for order in range(self._min_order, self._max_order + 1): | ||
| newval = 0 | ||
| for i in range(ibegin, ibegin + order): | ||
| x = a + 0.5 * (b - a) * (self._roots_mv[i] + 1.) | ||
| newval += self._weights_mv[i] * func.evaluate(x) | ||
| newval *= 0.5 * (b - a) | ||
|
|
There was a problem hiding this comment.
a + 0.5 * (b - a) and 0.5 * (b - a) are constants over the iteration, please consider calculating the values outside of the cycles. If you also apply the comment suggesting to store root + 1 value in self._roots the code would speed up a bit. Considering how many times the integrate method can be called in an observation, any speedup would be I think welcomed (regarding the fact that it is for minimum amount of work)
| Compute a definite integral of a one-dimensional function. | ||
| """ | ||
|
|
||
| cpdef (double, double) integrate(self, Function1D func, double a, double b): |
There was a problem hiding this comment.
please consider renaming the integrate method to evaluate, which is used in the function framework. Also, why not to add the python __call__ method, as is done in the function framework?
Also, is returning the error necessary and what we want? The maximum value of the error can be controlled and the error value is not needed during observations, where you are throwing out the value anyway.
There was a problem hiding this comment.
The Integrator1D now inherits from the Function2D. I renamed integrate to evaluate and now this method returns a single value. Now the __call__ method from the parent class can be used.
| def __init__(self, Line line, double wavelength, Species target_species, Plasma plasma, Integrator1D integrator=None, | ||
| dict stark_model_coefficients=None): |
There was a problem hiding this comment.
Please consider putting integrator parameter at the end for backwards compatibility
|
Thank you very much for the review, @Mateasek. I've implemented all your suggestions.
|
jacklovell
left a comment
There was a problem hiding this comment.
I like this a lot: having more specialised integration routines to supplement trapezoidal integration is very useful indeed.
However, I don't think it makes sense to subclass the function framework classes for these integrators: just because Integrator1D.evaluate is a function of 2 arguments doesn't make the integrator a 2D function. For example, doing Integrator1D(function) + Arg1D() makes no sense.
Cherab has precedent for this: the core rate objects don't subclass from Function2D, even though they have evaluate(ne, te) methods. I think the same should apply here.
Otherwise, looks good. Couple of minor comments addressed inline.
| if self.function is None: | ||
| raise AttributeError("Integrand is not set.") |
There was a problem hiding this comment.
Is there an expected use case for these integrators where the integrand is None? Ideally you would fail earlier than at the evaluation point: maybe require an integrand to be passed in when the object is instantiated and assert in the property setter that it cannot be None.
There was a problem hiding this comment.
If Integrator1D is not a subclass of Function2D, then I see no point in making function an attribute of the class. I think it's better to pass it as an argument to evaluate():
cdef evaluate(self, Function1D function, double a, double b):The wrapping can be done in the __call__ then:
def __call__(self, object function, double a, double b):
return self.evaluate(autowrap_function1d(func), a, b)There was a problem hiding this comment.
Performing autowrap in every call could lead to a performance degradation if this is called many times: autowrap does a sequence of type checks and type conversions to validate the input. If these integrators are called many fewer times than the underlying function the performance hit is likely to be small though, so I don't see this as a major problem unless you can think of a use case where one might be for example sampling the output of the integral on a high resolution grid.
Ultimately I think either way is fine, but using the property you're effectively caching the result of the validations done using autowrap.
There was a problem hiding this comment.
I would keep the function as a property. The integration is going to be called many times during observations
There was a problem hiding this comment.
The performance of the Cython code will not suffer because evaluate is called directly in Cython, and the wrapping is done only in the Python __call__ method. But never mind, let's leave the integrand as a property.
The problem is that we can't make it mandatory to set the integrand on initialisation. This is so because an instance of Integrator1D is passed to the LineShapeModel constructor as an argument, but the integrand cannot be set prior LineShapeModel initialisation. If setting the integrand to None is a bad option, then it should have some default value, for example Constant1D(0).
…lised line shape.
|
Thanks for the review, @jacklovell. I fixed the issues you mentioned. Also, I've added analytical line shape normalisation to |
This PR fixes #366 and makes the following changes:
Integrator1Dclass for calculating definite integrals ofFunction1D,GaussianQuadratureintegrator,NumericallyIntegrableLineShapeModelclass for lineshapes that cannot be analytically integrated over a spectral bin,StarkBroadenedLine.add_line()with integration over spectral bins.