8000 NFC-HOA second-order-section filters by narahahn · Pull Request #45 · sfstoolbox/sfs-python · GitHub
[go: up one dir, main page]

Skip to content

NFC-HOA second-order-section filters #45

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 11, 2019
Merged

NFC-HOA second-order-section filters #45

merged 1 commit into from
Mar 11, 2019

Conversation

narahahn
Copy link
Member
@narahahn narahahn commented Nov 8, 2017

NFC-HOA driving functions are implemented using first/second-order section filters.

ps_level ps_soundfield
pw_level pw_soundfield

To be discussed

  • The SOS filter coefficients are stored in a dictionary (see below). Is there a more elegant way?
sos = {}
for m in range(0, max_order+1):
     ...
     sos[m] = sig.zpk2sos(z0, zinf, k, pairing='nearest')
  • I realized that the Ambisonic order can be increased beyond 150 by doing some hack on scipy.signal.besselap. But I'm not so convinced whether this is really necessary, considering that the cut-off frequency for 150th order is already around 20 kHz.
  • So far, the poles and zeros in the Laplace domain is converted into the z-domain by using the matched-z transform. If necessary, bilinear transform can be added.

Copy link
Member
@hagenw hagenw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, thanks.
I will start with a few random points I have spotted.


def _max_order_circular_harmonics(N, max_order):
"""Compute order of 2D HOA."""
return (N-1) // 2 if max_order is None else max_order
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is one of the functions we need for the time domain and the mono-frequent implementation. It is already implemented mono/drivingfunction.py, but in a slight different way:

def _max_order_circular_harmonics(N, max_order):
    """Compute order of 2D HOA."""
    return N // 2 if max_order is None else max_order

Having a look at Ahrens (2012), p. 132 there should also be a distinction of cases for even and odd numbers:

     / N/2 - 1,   even nls
M = <
     \ (N-1)/2    odd nls

Another problem is that this only holds for 2D and 2.5D. For 3D we would have:

      _____
M = \|nls/2

see also nfchoa_order.m.
As we don't have 3D at the moment we could stick with the function name and change it later if we need 3D.

TODOS:

  • find a common place and define only one _max_order_circular_harmonics(N, max_order) function
  • add distinction between odd and even N
  • add a reference to the help message

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @narahahn,
I just realized that (N-1) // 2 is already the correct implementation of the max order function.
And the 2D vs. 3D problem can be solved by naming the 3D case _max_order_spherical_harmonics.
From the mentioned TODOs, then only these remain:

  • find a common place and define only one _max_order_circular_harmonics(N, max_order) function
  • add a reference to the help message

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. Maybe in sfs.util?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think sfs.util would be currently the best fit.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While you are at it, you could replace the if/else expression with an if/else statement, I think this would be easier to understand.

Then it should also become more obvious that you should remove the separate check for None before calling the function.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to implement this with ef78d76, where I moved _max_order_circular_harmonics to util, renamed it to max_order_circular_harmonics and added documentation.

For the documentation I would need some help. At the moment I added page 132 of [Ahrens2012]_, but it would be nicer to replace this by the actual equation. As I don't have a copy of the book here I cannot do this myself.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need the equation number? It's (4.26)

phaseshift : float
Phase shift

"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a reference to the help message. In sfs-matlab we used eq. (11) from Spors, Kuscher, Ahrens (2011).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the review.
The references are now added with the respective equation numbers.

phaseshift : float
Phase shift

"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a reference to the help message. In sfs-matlab we used eq. (10) from Spors, Kuscher, Ahrens (2011).

for channel, cdelay in enumerate(delays_samples):
out[cdelay:cdelay + len(signal), channel] = signal
return out, offset_samples / fs

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will break the current implementation of the time based WFS driving functions.
I think we wanted to try out using the signal class, here is an example : https://github.com/sfstoolbox/sfs-python/blob/master/sfs/util.py#L141

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you. Probably I chose the wrong script while merging.

@hagenw hagenw mentioned this pull request Nov 9, 2017
@mgeier
Copy link
Member
mgeier commented Nov 10, 2017
$ python3 -m pycodestyle sfs/time/drivingfunction.py
sfs/time/drivingfunction.py:261:80: E501 line too long (84 > 79 characters)
sfs/time/drivingfunction.py:284:37: W291 trailing whitespace
sfs/time/drivingfunction.py:287:1: W293 blank line contains whitespace
sfs/time/drivingfunction.py:293:1: W293 blank line contains whitespace
sfs/time/drivingfunction.py:297:20: E225 missing whitespace around operator
sfs/time/drivingfunction.py:305:1: W293 blank line contains whitespace
sfs/time/drivingfunction.py:327:80: E501 line too long (83 > 79 characters)
sfs/time/drivingfunction.py:350:37: W291 trailing whitespace
sfs/time/drivingfunction.py:354:1: W293 blank line contains whitespace
sfs/time/drivingfunction.py:360:1: W293 blank line contains whitespace
sfs/time/drivingfunction.py:364:20: E225 missing whitespace around operator
sfs/time/drivingfunction.py:388:32: E231 missing whitespace after ','
sfs/time/drivingfunction.py:388:37: E231 missing whitespace after ','
sfs/time/drivingfunction.py:388:39: E231 missing whitespace after ','
sfs/time/drivingfunction.py:394:80: E501 line too long (93 > 79 characters)
sfs/time/drivingfunction.py:404:37: W291 trailing whitespace
sfs/time/drivingfunction.py:421:20: E225 missing whitespace around operator
sfs/time/drivingfunction.py:429:20: E231 missing whitespace after ','
sfs/time/drivingfunction.py:429:23: E231 missing whitespace after ','
sfs/time/drivingfunction.py:433:12: E231 missing whitespace after ','
sfs/time/drivingfunction.py:437:16: E231 missing whitespace after ','
sfs/time/drivingfunction.py:440:1: W293 blank line contains whitespace
sfs/time/drivingfunction.py:441:1: E302 expected 2 blank lines, found 1
sfs/time/drivingfunction.py:445:1: E302 expected 2 blank lines, found 1
sfs/time/drivingfunction.py:457:1: W391 blank line at end of file

Those are not all in your changes, I'm just trying to say that you should use some kind of a linter, ideally as an editor plugin or something.

@mgeier
Copy link
Member
mgeier commented Nov 30, 2017

The SOS filter coefficients are stored in a dictionary [...]. Is there a more elegant way?

Will there ever be some orders missing?

If not, I think a simple list would be better, because it can simply be indexed from 0 to the maximum order.
And it can be easily iterated in a well-defined order.

But if there is ever an order missing (or not starting from 0), this wouldn't work. Then a dict would make sense IMHO.

@narahahn
Copy link
Member Author

@mgeier Thank you, it works very well. Just simple.
And I don't think we'll ever use missing-order-Ambisonics :-)

@mgeier
Copy link
Member
mgeier commented Dec 1, 2017
$ python3 -m pydocstyle sfs/time/drivingfunction.py 
sfs/time/drivingfunction.py:261 in public function `nfchoa_25d_plane`:
        D413: Missing blan
9E19
k line after last section ('References')
sfs/time/drivingfunction.py:326 in public function `nfchoa_25d_point`:
        D207: Docstring is under-indented
sfs/time/drivingfunction.py:326 in public function `nfchoa_25d_point`:
        D413: Missing blank line after last section ('References')
sfs/time/drivingfunction.py:438 in private function `_normalize_digital_filter_gain`:
        D202: No blank lines allowed after function docstring (found 1)
sfs/time/drivingfunction.py:438 in private function `_normalize_digital_filter_gain`:
        D400: First line should end with a period (not 'y')

s0 = (c/r)*p
sinf = (c/r0)*p
z0 = np.exp(s0/fs)
zinf = np.exp(sinf/fs)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add the used equations to the docstring.

If it makes sense, you can also move this to a separate function (and document the equations there).

@mgeier
Copy link
Member
mgeier commented Feb 19, 2019

Thanks for the updates!

I've marked all my outdated requests as "resolved" and added a few more comments.

Apart from this, is this PR ready for merging?

If yes, can you please squash all commits and rebase to master?

@fs446
Copy link
Member
fs446 commented Feb 19, 2019

For nfchoa_25d_point() and all other related time domain NFC-HOA (and maybe somewhere else?)
the current usage denotation of s0, sinf, z0, zinf
k = _normalize_digital_filter_gain(s0, sinf, z0, zinf, fs)
might be changed to
k = _normalize_digital_filter_gain(szero, spole, zzero, zpole, fs)
for improved readability. Although coding becomes slightly longer the consistency, self-explanation and same-length convention could be helpful.

@narahahn
Copy link
Member Author

_matchedz_zpk() now computes the gain zgain of the SOS filters.
This makes the behavior consistant with bilinear_zpk(),
in terms of input and output variables.

Variable names sgain and zgain are used instead of k
for the system gain in the s- and z-domain, respectively.
So the system function is described by {szero, spole, sgain},
and {zzero, zpole, zgain}.

The gain normalization is now performed inside _matchedz_zpk().
This makes _normalize_digital_filter_gain() obsolete.
It will be removed after some tests.

@mgeier
Copy link
Member
mgeier commented Feb 21, 2019

I agree that zzero is better than z0, but what about z_zero?
I think it would be more readable with an underscore.

Also, shouldn't it be plural?

z_zeros, z_poles, s_zeros, s_poles, z_gain, s_gain

This makes the behavior consistant with bilinear_zpk(),
in terms of input and output variables.

That's great! Now you can get rid of the ugly strings and just pass functions around.
You should also change _matchedz_zpk() to matchedz_zpk() for that.

This, in turn, makes the appendix _method obsolete (and slightly wrong).

The signature could then look like this:

def nfchoa_25d_point(x0, r0, xs, fs, max_order=None, c=None, s2z=matchedz_zpk):

@narahahn
Copy link
Member Author

I agree that zzero is better than z0, but what about z_zero?
I think it would be more readable with an underscore.

I also prefer using underscore,
but then I would like to propose yet another convention:

zdomain_zeros, sdomain_poles

which in my opinion reads better and is more self explanatory.
The single letter z might be misunderstood as zero (like in zpk)
or the z-coordinate.

That's great! Now you can get rid of the ugly strings and just pass functions around.

Great idea, thanks!
Then we don't need the if-else statement
and can just call either function by s2k(z, p, k, fs), right?

@narahahn
Copy link
Member Author

The signature could then look like this:

def nfchoa_25d_point(x0, r0, xs, fs, max_order=None, c=None, s2z=matchedz_zpk):

Hmm ... This doesn't work because matchedz_zpk and bilinear_zpk
are not defined in the current workspace where nfchoa_25d_point() is called.
Any idea?

@mgeier
Copy link
Member
mgeier commented Feb 21, 2019

zdomain_zeros, sdomain_poles

Well at some point we'll have to stop making the names longer ...

This would have to be z_domain_zeros to be consistent, and the two underscores make it a bit tedious in my opinion. But I think it would still be acceptable.

If you prefer it, that's fine for me.

The single letter z might be misunderstood as zero (like in zpk)
or the z-coordinate.

That's a risk that I would be willing to take.

Then we don't need the if-else statement and can just call either function by s2k(z, p, k, fs), right?

Exactly.

Hmm ... This doesn't work because matchedz_zpk and bilinear_zpk
are not defined in the current workspace where nfchoa_25d_point() is called.

Do you mean where it is called or where it is defined?

The default argument must be available where the function is defined. That means that you'll have to move the definition of matchedz_zpk() further up in the file.
Or, probably better, move it to an entirely different place (e.g. sfs.util?) and import it in drivingfunction.py.

OTOH, bilinear_zpk doesn't have to be defined anywhere. It's the responsibility of a user to import it from scipy.signal.
It would probably make sense to mention this in the documentation.

@narahahn
Copy link
Member Author

This would have to be z_domain_zeros to be consistent, and the two underscores make it a bit tedious in my opinion. But I think it would still be acceptable.

We are already using variable/function names like
samplerate, drivingfunction, soundfield, wfs_25d_preeq.
I think zdomain_ isn't so inconsistent.
BTW, I wouldn't use two underscores, either.

The default argument must be available where the function is defined. That means that you'll have to move the definition of matchedz_zpk() further up in the file.
Or, probably better, move it to an entirely different place (e.g. sfs.util?) and import it in drivingfunction.py.

Although passing the functions is very a fancy idea,
it could be a bit inconvenient or even confusing
for non-expert users like me.

From

def nfchoa_25d_point(x0, r0, xs, fs, max_order=None, c=None, s2z=matchedz_zpk):

I would expect that

nfchoa_25d_point(x0, r0, xs, fs, max_order, c):

and

nfchoa_25d_point(x0, r0, xs, fs, max_order, c, s2z=matchedz_zpk)

are the same, but actually the second one will work only if
matchedz_zpk is defined where the driving function is called.

from sfs.util import matchedz_zpk
d = sfs.time.drivingfunction.nfchoa_25d_point(
    x0, r0, xs, fs, max_order, c, s2z=matchedz_zpk)

or

d = sfs.time.drivingfunction.nfchoa_25d_point(
    x0, r0, xs, fs, max_order, c, s2z=sfs.time.drivingfunction.matched_zpk)

@mgeier
Copy link
Member
mgeier commented Feb 26, 2019

I think zdomain_ isn't so inconsistent.

It is inconsistent in the sense that the first two words are directly joined together, while the second and third word are joined by an underscore.

I think we should choose one or the other.

But PEP-8 doesn't say anything about this: https://www.python.org/dev/peps/pep-0008/#function-and-variable-names

If you really prefer zdomain_zeros, I'm fine with it.
I find it awkward, but I can live with it.

but actually the second one will work only if
matchedz_zpk is defined where the driving function is called.

Is it also confusing that x0, r0, xs and fs have to be defined where the driving function is called?

This is just how Python works: names have to be defined.

And function definition may happen in a different namespace than function use.

Users will have to know a few Python features in order to use the SFS toolbox. And if they don't know those features, we can give them an opportunity to learn, which is great!

@narahahn
Copy link
Member Author

3D NFC-HOA driving functions for virtual plane waves and point sources are added.
Synthesized sound fields of virtual plane waves are shown below.

pw_soundfield

pw_soundfield

The source signals are band-limited impulses (22 kHz and 1 kHz).
240 secondary sources are placed on a sphere with R=1.5 m.
I used pre-calculated t-design distribution from
http://neilsloane.com/sphdesigns/dim3/

@narahahn
Copy link
Member Author

Equations describing the NFC-HOA driving functions are added in the docstring (c600808).
Although the equations and the respective functions are
intended for time-domain implementations,
the driving functions are represented in the Laplace domain
from which the filter coefficients are computed.
However, this looks somewhat inconsistent considering that
the WFS driving functions are all represented in the time-domain.
We may evaluate the inverse Laplace transform and use the
time-domain representation, but IMO, this would be misleading
since we never use it.
Do you have any thoughts?

@mgeier
Can I use an in-line LaTeX sytax in the docstring?
I want to define some variables appearing in the equations.

1241

@mgeier
Copy link
Member
mgeier commented Feb 28, 2019

Can I use an in-line LaTeX sytax in the docstring?

Yes you can and you should!

You can use inline math with :math:`x` and display math with

.. math::

    x

Just don't forget to use a raw string r"""...""" if you use backslashes (which I guess you'll do).

@narahahn
Copy link
Member Author
narahahn commented Mar 1, 2019

You can use inline math with :math:`x` and display math with

.. math::

    x

Great! Thank you.

< 10000 /details-collapsible>
Copy link
Member
@mgeier mgeier left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update! Now nearly all my comments are addressed, time to add some more ...

Is it intentional that sequences of zeros and poles are called "zero" and "pole" in singular?

@narahahn
Copy link
Member Author
narahahn commented Mar 8, 2019

Is it intentional that sequences of zeros and poles are called "zero" and "pole" in singular?

Sorry, I missed that. They're changed now.

Copy link
Member
@mgeier mgeier left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, that's great, I really like it.

Now the only problem is that there were some API changes in master (mainly driving functions returning some more stuff) ... could you please update your PR accordingly?

@narahahn
Copy link
Member Author

Thanks, that's great, I really like it.

I really appreciate your review and comments!

Merging from the master branch was really the worst idea.
Rewriting the history was quite painful.

Now the only problem is that there were some API changes in master (mainly driving functions returning some more stuff) ... could you please update your PR accordingly?

The code are updated according to the recent changes.

But when I build the HTML pages, the math symbols like \i, \e are not properly displayed.
Same result in the master branch. Is there some work in progress that might cause this?

@mgeier mgeier merged commit 4395c15 into master Mar 11, 2019
@mgeier
Copy link
Member
mgeier commented Mar 11, 2019

Thanks a lot @narahahn, I think it's time to merge this!

The documentation for the new return values is missing, but we can add this at a later time.

Merging from the master branch was really the worst idea.

Well some people like this workflow and they keep all those merge commits in their history. I don't like this, because I think it makes the history basically useless.
I prefer doing git rebase master regularly, and when there are too many commits in the feature branch, squashing some of them with git rebase --interactive.

But anyway, now it's done, which is great!

Is there some work in progress that might cause this?

I think this is a problem with your local files, I don't see this problem. You should probably remove the old "build" directory and try again.

@mgeier mgeier deleted the nfchoa_sosfilter branch March 11, 2019 10:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants
0