Skip to content

Conversation

@ziotom78
Copy link
Member

@ziotom78 ziotom78 commented Aug 1, 2020

This PR implements a pointing-generation code, i.e., a set of routines that take a scanning strategy definition as input and produce a set of direction in the sky and polarization angles as output. Once the PR is finished, I expect it to be used in the following way:

import litebird_sim as lbs

sim = lbs.Simulation(…)
sim.create_observations(…)
sim.generate_pointing_information(imo_object="/releases/v1.0/Satellite/scanning_information/")

# Now pointing information are available in each "Observation" object

The method generate_pointing_information works as follows:

  1. It creates a set of quaternions representing the transformation from the boresight reference frame to Ecliptic coordinates; this is kept in a matrix whose shape is N x 4 and using the scalar-last convention, which is compatible with the data layout used by ducc (see github.com/litebird/ducc/pull/1).
  2. Depending whether the flag use_mjd has been set or not in the call to sim.create_observations, the transformation to Ecliptic coordinates uses a barycentric true ecliptic coordinate system and the true Sun-Earth direction (using astropy.coordinates) or a quick-and-simple geometrical representation of the Ecliptic plane.
  3. The set of quaternions is calculated at a smaller sampling rate than the scientific samples. At the moment they are sampled once every 60 s, which means that 4 years of mission require ~64 MB for all the quaternions, but this parameter is tunable.
  4. When the user needs pointings, they are calculated using the method PointingProvider.get_rotated_quaternions implemented in ducc (see this example).
  5. It is possible to avoid using a Simulation object and generating the pointings for just one observation using Observation.generate_pointing_information. This can be handy for debugging and is already used in the unit tests.

Things to do:

  • Implement basic quaternion operations
  • Implement a ScanningStrategy object
  • Compose quaternion operations to convert from the boresight reference frame to the Sun-Earth axis frame
  • Apply the appropriate conversion from the Sun-Earth axis frame to the Ecliptic frame, using ephemerides whenever use_mjd=True
  • Write tests to check that quaternions are generated correctly
  • Write the code that retrieves the scanning strategy from the IMO
  • Implement Simulation.generate_pointing_information so that it iterates over all the observations and calls Observation.generate_pointing_information for each of them
  • Use ducc to generate pointing information and polarization angles at the same sampling rate as the data
  • Write tests to check that the pointing information and polarization angles are correct
  • Write docstrings
  • Write the documentation
  • Provide a way to use different boresight definitions (LFT/MFT/HFT)
  • Find a way to fix the warnings produced by ERFA about «dubious years» (see github.com/astropy/astropy/issues/5809 for an explanation of the cause)

@ziotom78
Copy link
Member Author

ziotom78 commented Aug 1, 2020

@mreineck , I am having problems compiling ducc on Mac OS X, see here: https://travis-ci.com/github/litebird/litebird_sim/jobs/367296483. I discovered that it does not compile using Ubuntu 18.04 LTS because the GCC is too old, and I suspect that the issue we're experiencing here with Mac OS X has the same origin.

If you tried to set up a Travis CI job for the library, can you share your scripts so that I can integrate your solution here? At the moment the PR does not use ducc, but it will do it soon.

@mreineck
Copy link
Collaborator

mreineck commented Aug 1, 2020

Are you actually using gcc/g++ as the compilers here? The Travis log only mentions clang, and also I'd never expect gcc to say something like XYZ is unavailable: introduced in macOS 10.14. I don't understand the statement that the OS somehow determines which language features are supported by a compiler; I can only assume that the system's default compiler is not new enough to support C++17. But there should be no problem installing more recent compilers.

Any g++ and clang with C++17 support should do; ccording to Wikipedia this is g++ 7 or later and clang 5 or later (but the MacOS system clang seems to follow a different versioning scheme than the official one ...)

I have set up CI with Gitlab, will attach the relevant files in a minute!

@mreineck
Copy link
Collaborator

mreineck commented Aug 1, 2020

No need to upload anything ... the files are Dockerfile and .gitlab-ci.yml in the ducc repo.

@ziotom78
Copy link
Member Author

ziotom78 commented Aug 1, 2020

Are you actually using gcc/g++ as the compilers here?

I have just asked TravisCI to install XCode and use whatever is available; I think that this makes the VM defaulting to clang. I am going to bump the requirement on XCode from 11.0 to 12.0, let's hope that this fixes the build.

I don't understand the statement that the OS somehow determines which language features are supported by a compiler; I can only assume that the system's default compiler is not new enough to support C++17

It sounds troublesome at best… I found nothing else than a Reddit topic about this: https://www.reddit.com/r/cpp_questions/comments/9la2e7/technical_reasons_for_apple_restricting_any_and/

@ziotom78
Copy link
Member Author

ziotom78 commented Aug 1, 2020

Ok, this build failed too:

https://travis-ci.com/github/litebird/litebird_sim/jobs/367303706

It seems you're right: even if the SDK is the most recent one, it fails to build because the OS does not support some functionalities required by the C++ library, as this error message suggest:

/Library/Developer/CommandLineTools/SDKs/MacOSX10.15.sdk/usr/include/malloc/_malloc.h:50:10: note: 'aligned_alloc' has been marked as being introduced in macOS 10.15 here, but the deployment target is macOS 10.9.0

(So Mac OS X 10.9 doesn't provide functions to allocate a block of aligned memory… really?!?)

However, this is in conflict with the TravisCI documentation (https://docs.travis-ci.com/user/reference/osx#macos-version), which states that specifying xcode12 in the Travis script will use Mac OS X 10.15.

I think we need some help from people using the Mac. @dpole , might you please try to download the pointings branch and check that you can run the tests?

@mreineck
Copy link
Collaborator

mreineck commented Aug 1, 2020

(So Mac OS X 10.9 doesn't provide functions to allocate a block of aligned memory… really?!?)

I'm pretty sure it provides the function, but htis message is about the aligned_alloc function, which was added in C++17 and is part of the C++ standard library.
Is it possible that on MacOS, all C++ binaries are linked to the system provided C++ standard library, and not the one that comes with the compiler? If so, this message makes at least remote sense, because then it wouldn't matter how new your clang is, because in order to use aligned_alloc you also need a recent standard library. And if the custom is to link to the outdated, system-provided one, this will fail.

In any case, I cannot imagine that this kind of trickery is going on when g++ is used.

@mreineck
Copy link
Collaborator

mreineck commented Aug 1, 2020

BTW, I'm sorry that this code is proving such a hassle to compile... after your success on Windows, I definitely didn't expect this. I'm also building the library regularly with clang under Linux, and that also works perfectly well.
(OK, to be honest, I had to switch off CI tests with clang recently, but that's because Debian testing currently has their dependencies wrong. That should be fixed soon, however.)

@ziotom78
Copy link
Member Author

ziotom78 commented Aug 2, 2020

Is it possible that on MacOS, all C++ binaries are linked to the system provided C++ standard library, and not the one that comes with the compiler?

Yeah, I believe so, this would explain everything. What puzzles me is the fact that Travis should be using Mac OS X 10.15, not Mac OS X 10.9.

BTW, I'm sorry that this code is proving such a hassle to compile...

Don't worry, this is the very first C++ library we're integrating in the code, so it makes sense that we're experiencing many problems. Once we're able to understand how to make it work, I expect that other C++ libraries will be far simpler to use.

@mreineck
Copy link
Collaborator

mreineck commented Aug 2, 2020

What puzzles me is the fact that Travis should be using Mac OS X 10.15, not Mac OS X 10.9.

I wonder if this is related to the fact that my setup.py required a minimum version of 10.9...
This is now updated to 10.15 ... with a bit of luck it could work now.

@mreineck
Copy link
Collaborator

mreineck commented Aug 2, 2020

Yes! I think that did the trick :)

@ziotom78
Copy link
Member Author

ziotom78 commented Aug 2, 2020

Yes! I think that did the trick :)

You're my hero, thanks a lot!

@mreineck
Copy link
Collaborator

mreineck commented Aug 3, 2020

I still don't feel particularly good about requiring such a recent OS version... I mean, recent compiler versions are easier to install than newer OSs. What do you think, should I try to avoid the aligned_alloc call or can we reasonably expect that our users will be on MacOS 10.15?

@ziotom78
Copy link
Member Author

ziotom78 commented Aug 3, 2020

What do you think, should I try to avoid the aligned_alloc call or can we reasonably expect that our users will be on MacOS 10.15?

Mmm, this depends on how often Apple users upgrade their OS. I guess they do it promptly, but it's probably better to ask to other members of the ST that are Apple users if this is true (I know of @NicolettaK , @paganol , @dpole , but there are surely many others).

This problem is going to be less and less relevant as time passes, considering that Apple has just announced the successor to 10.15, and that Apple is famous for quickly dropping support for older OSes. (Wikipedia extrapolates old data to guess that 10.14 will be supported till September 2021, i.e., one year from now.) I would go for dropping support for Mac OS X 10.14 and earlier versions, but let's hear for other opinions.

@NicolettaK
Copy link
Contributor

Ehm, I've just checked and I still have Mac OS X 10.14...

@ziotom78
Copy link
Member Author

ziotom78 commented Aug 3, 2020

Ehm, I've just checked and I still have Mac OS X 10.14...

Argh, then perhaps you could use something like

#ifdef __APPLE__
…
#else
…
#endif

to use aligned_alloc only on non-Apple platforms, at least until 10.14 goes out of support. (Don't know if more sophisticated tests that check for 10.15 and newer are worth the effort.)

@mreineck
Copy link
Collaborator

mreineck commented Aug 3, 2020

I'm trying exactly this right now. The problem is that C++17 not only provides aligned_alloc, it also guarantees that data types which require special alignment (like SIMD types) will autimatically get this alignment even on the heap, e.g. if I have a std::vector<simd_type>, that the allocated memory for the vector will be sufficiently aligned. No such guarantee was made before C++17.

So if Apple could not be bothered to introduce the aligned_alloc function before MAcOS 10.15, I'm not too confident that they will have implemented this subtle language change...

@mreineck
Copy link
Collaborator

mreineck commented Aug 3, 2020

OK, I'm falling back to some emulation of aligned_alloc when __APPLE__ is defined, and I reduced the version requirement to 10.14. So far it seems to work.

@dpole
Copy link
Member

dpole commented Aug 10, 2020

Hi @dpole and @mreineck , thanks for the comments! A few of the functions in this PR (which is still WIP) are not meant to be used by the end user (like quat_right_multiply), and a few of them are useless and will be removed (rotate_vector, all_rotate_vector). This kind of low-level functions is kept as simple as possible to help Numba in function inlining, although I have not checked yet how efficient is the code it generates. (I hope it is not much inefficient, I avoided using broadcasting to increase data locality in computations.)

Yes, I am not a numba expert and can not tell what level of numpy numba is able to digest.
About locality, I don't think that braodcasting should negatively affect it. I actually think it improves it. If locality is a concern, I'd start by requiring quaternions to be in a length-4 array (I would do that in any case)

Regarding scipy.rotation, I wasn't aware of it. Consider however that the scanning strategy generator only uses rotate_x_vector, rotate_x_vector, and rotate_x_vector, which are optimized for the special cases where you rotate around the x, y, and z axis. It doesn't seem that scipy.rotation provides the same.

I wasn't aware either until this morning, when I started googling because I couldn't believe that there wasn't any standard library for quaternion operations. I should say that I'm not sure it is mature enough to be considered standard but, skimming through the source, it seems to me that all the functionalities we need are there. Rotator is essentially a quaternion container with a bunch of methods for compositions, representation, rotation application etc.

About broadcasting, it would be completely useless for the current implementation, which does not use broadcasting anywhere. But if you believe it could be useful for the end user, I can add the functionality to quat_right_multiply (@dpole, when you write ... in your example code, are you using an actual feature of Numba? I was not aware of this syntax.)

From a quick look I believe that it could be useful already in the current implementation. I presume that broadcasting would percolate from quat_*_multiply all the way to all_boresight_to_ecliptic and make it redundant, as broadcasting would automatically take care of the for loop.

... in the indexing is a standard numpy indexing thing, not numba's. It allows indexing the last dimension of an ndarray
The only advantage that my proposal would have over the scipy.spatial.transform.rotation._compose_quat is that it would allow any broadcasting, not just 2-D broadcasting (Rotation allows only for Nx4 quaternions)

@ziotom78
Copy link
Member Author

About locality, I don't think that braodcasting should negatively affect it. I actually think it improves it.

Mmm, I'm doing a few tests using broadcasting and the scipy rotation matrices, but my Numba code seems to be ~2 times faster in calling all_compute_pointing_and_polangle (source code in this gist):

In [1]: %run scipy_rotations_test.py
Consistency test: True

In [2]: %timeit test_lbs(quat_matrix)
1.55 ms ± 12.2 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

In [3]: %timeit test_scipy(quat_matrix)
3.48 ms ± 18.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Introducing broadcasting without causing serious performance regressions does not seem trivial; at least, not for me. I don't have time to investigate this, and the Numba code I wrote so far works well (all the results look mathematically correct), so for the moment I'll leave everything as it is. We can always introduce broadcasting later, once we find the culprit.

The "Observation" class should be completely unaware of the details of the
scanning strategy: it's not its duty to know that a spinning axis and a
precession axis exist at all.  Moreover, the code as it was before this
commit was not efficient: every observation generated the
boresight-to-ecliptic quaternions, even if multiple observations could in
principle overlap the same time frame (e.g., an Observation for 30 GHz
detectors and another for 40 GHz detectors, both spanning the same time
interval) and thus have the very same set of quaternions.

This commit is clearly separates responsibilities and, although not as
efficient as it could, it enables a number of niceties and future
optimizations:

-   The set of quaternions is generated by a method of the
    `ScanningStrategy` class, no longer in the `Observation` class
    (`ScanningStrategy.generate_bore2ecliptic_quaternions`).  This is the
    most obvious place where this computation should happen, and it does not
    prevent other scanning strategies from being implemented (e.g.,
    dedicated follow-up of calibration sources).

-   The method `Simulation.generate_pointing_information` simply wraps the
    `generate_bore2ecliptic_quaternions` above

-   Because of the previous point, the `Simulation` class needs to
    know when the simulation starts and how long it lasts.  Therefore, two
    new fields `start_time` and `duration_s` have been added to the
    `Simulation` class and to its constructor (previously they were passed
    to `Simulation.create_observations`).  This is more elegant, as these
    fields are stored in the `Simulation` class for future reference and are
    included in the header of the report generated at the end of the
    simulation.

-   The method `Observation.get_pointings` now requires a set of
    quaternions to be slerped.  A simple quaternion matrix is not enough
    here, because you want to know the time of the first quaternion (e.g.,
    2027-01-01) and the time interval between consecutive rows of the
    matrix, I have created a `Bore2EclipticQuaternions` class that collects
    the three fields together.  This class is returned by the method
    `ScanningStrategy.generate_bore2ecliptic_quaternions` and is stored in
    the `Simulation` object.  I foresee that other classes in the future
    could create this object, e.g.,
    `PlanetFollowUp.generate_bore2ecliptic_quaternions`: this will be
    compatible with the way this method `Observation.get_pointings` works,
    which is a big advantage.

-   It should not be difficult to parallelize the call to
    `Simulation.generate_pointing_information`: each MPI process should
    generate a subset of quaternions, which would be put together in a large
    matrix at the end.  This commit does not attempt to do so.
@dpole
Copy link
Member

dpole commented Aug 12, 2020

From what I could understand you are moving some data from the observations to other objects.

I believe that it would be good the keep the observation as the only class responsible for the distributed time-domain or detector-domain data (actually I would use this as the definition of what the Observation class should do). The idea is to keep the complexity of chunking and distributing this information in one place (the Observation class) and have other classes (possibly functions) that take data from the observations and operate over it.

What do you think?
[I skimmed through your latest changes quickly and may have missed something, sorry about that]

@dpole
Copy link
Member

dpole commented Aug 12, 2020

About quaternions routines, I'm fine with that. The scipy implementation doesn't look like the most efficient thing one can do and your test demonstrates it. What I had in mind was to write broadcast-compatible versions of your routines, but I'd keep it as a low priority thing. I'll propose a patch if I have time.

@ziotom78
Copy link
Member Author

I believe that it would be good the keep the observation as the only class responsible for the distributed time-domain or detector-domain data.

I agree. In fact this is still the approach used in the latest commits.

A few improvements:

-   Types and default values are specified for all the parameters in the
    constructor.  This should make the documentation page for pointing
    generation easier to read and shorter.

-   The functions `read_detector_from_dict` and `read_detector_from_imo`
    have been replaced with class methods, which are shorter to type, more
    elegant, and they match the way class `ScanningStrategy` behaves.
    Moreover, there are less names to be exported in __init__.py
Force the "pixel" to be an integer number and
add a docstring.
The changes to the code in this commit have the only purpose
to make the API easier to understand; all of them stem from the
very act of having written the documentation in
"docs/source/scanning.rst". A few examples:

1. The class `ScanningStrategy` has been renamed into
   `SpinningScanningStrategy` and a new base class
   `ScanningStrategy` has been introduced: this makes the
   development of alternative scanning strategies far easier

2. Related to point 1., a few lines of code that were put in
   the old `ScanningStrategy.generate_bore2ecl_quaternions` have
   been moved in new static methods of the base class
   `ScanningStrategy`

3. The method `Simulation.generate_pointing_information` has been
   renamed to `Simulation.generate_bore2ecl_quaternions`, which is
   more precise (no pointing information is computed in that method).

4. The `detector` class now accepts defaults for all its parameters,
   thus making the examples in the User's manual far shorter and
   clearer to explain.
The upgrade solves systematic crashes that were happening
on some systems using llvmlite 0.33.
The "Instrument" class is used to define the orientation of the boresight.
It permits to specify a tilt of the boresight as well as its phase angle
around the spin axis, which means that now the framework is fully able to
support a simulation of the three focal planes (LFT, MFT, HFT).  This comes
at the expense of moving the β parameter (boresight-to-spin-axis angle) out
of the "ScanningStrategy" class and into the "Instrument" class; it solves
an old inconsistency, as the angle β is not part of the «scanning strategy»,
being an hardware-specific parameter.

The dependencies have been updated:

1. The "sphinxcontrib-bibtex" library has been added; it's used to insert
   references to other papers using BibTeX.

2. The "ducc0" dependency now refers to the branch quat_rot_right, as it
   implements a new function needed for the quaternion composition used by
   the code. Once the "quat_rot_right" branch will be merged into "ducc0",
   this dependency will need to be updated accordingly.
The new method permits to simulate the observation of pointlike sources in
the sky.  As it depends on a few more quaternion functions, I have moved all
the Numba code dealing with quaternions in the new file
`litebird_sim/quaternions.py`.

Many docstrings have been improved, in particular those associated with
Numba functions.
@ziotom78
Copy link
Member Author

I believe the PR is now ready to be merged. The API will need to be revised once #42 is merged, but this can be done with a new PR. Please have a look at it, the documentation is available here:

https://litebird-sim.readthedocs.io/en/pointings/scanning.html

@ziotom78 ziotom78 merged commit f353362 into master Sep 3, 2020
@ziotom78 ziotom78 deleted the pointings branch September 3, 2020 09:16
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.

4 participants