## Introduction

It has pretty much always been assumed by a lot of people, including myself, that rendering engines are colourspace agnostic, and irrespective of what the primaries are, it should not make any differences.

An interesting thread with some VFX industry veterans about the [Academy Color Encoding System](http://www.oscars.org/science-technology/council/projects/aces.html) has been ignited by Steve Agland.

He was describing issues while rendering in ACES RGB colourspace where the rendering equation could generate some very saturated colours, colour clipping and loss of details. Since we have started this notebook, Steve has written a post illustrating those issues, I suggest that you take a look at his [notebook](http://nbviewer.jupyter.org/gist/sagland/3c791e79353673fd24fa).

As we were discussing, [Zap 'Master' Anderson](http://mentalraytips.blogspot.fr/) pointed that he was assuming primaries wouldn't matter but that it was wrong.

The [TL;DR; version](https://groups.google.com/d/msg/academyaces/9b4VuqPcOHQ/Hg8c9V6flOYJ) of *Rick Sayre* from [Pixar](http://www.pixar.com/)

> The RGB basis vectors typically become non-orthogonal when transformed to XYZ, and definitely so in this case.
Thus there should be no surprise that component-wise multiply does not yield a proper transform between two non-orthogonal spaces.

## Colourspace Agnosticism

We will demonstrate the issue in the following section, notice that it doesn't actually just only concern rendering but compositing, etc... Elle Stone has for example written an article about that problem in the context of image editing in [unbound *sRGB* colourspace](http://ninedegreesbelow.com/photography/unbounded-srgb-as-universal-working-space.html).

We choose some references colours in *CIE XYZ* colourspace (*dark skin* and *green*) from *ColorChecker 2005*. A unique colour is enough to showcase the issue but we will use two in order to be closer of the rendering context where multiple colours interact together. We can for instance assume that *dark skin* is a texture and *green* a light.

We convert them to *sRGB* and *Adobe RGB 1998* colourspaces. We have chosen those 2 colourspaces because they have the same whitepoint and thus discount chromatic adaptation, essentially removing a problem from the equation so that it is easier to understand:

```python
%matplotlib inline
```

```python
import numpy as np
import pylab

import colour
from colour.utilities.verbose import message_box

name, data, illuminant = colour.COLOURCHECKERS['ColorChecker 2005']

sRGB_w = colour.sRGB_COLOURSPACE.whitepoint
sRGB_XYZ_to_RGB = colour.sRGB_COLOURSPACE.XYZ_to_RGB_matrix
sRGB_RGB_to_XYZ = colour.sRGB_COLOURSPACE.RGB_to_XYZ_matrix
adobe98_w = colour.ADOBE_RGB_1998_COLOURSPACE.whitepoint
adobe98_XYZ_to_RGB = colour.ADOBE_RGB_1998_COLOURSPACE.XYZ_to_RGB_matrix
adobe98_RGB_to_XYZ = colour.ADOBE_RGB_1998_COLOURSPACE.RGB_to_XYZ_matrix

# Preparing *dark skin* colour in various colourspaces.
index, name, x, y, Y = data[0]
XYZ_r1 = colour.xyY_to_XYZ((x, y, Y))

# *sRGB* colourspace 0-255 values with OETF applied.
sRGB_rd1 = colour.XYZ_to_sRGB(XYZ_r1, illuminant)

# *sRGB* colourspace linear values.
sRGB_r1 = colour.XYZ_to_RGB(XYZ_r1,
                            illuminant,
                            sRGB_w,
                            sRGB_XYZ_to_RGB)

# *Adobe RGB 1998* colourspace linear values.
adobe98_r1 = colour.XYZ_to_RGB(XYZ_r1,
                               illuminant,
                               adobe98_w,
                               adobe98_XYZ_to_RGB)

message_box(('Reference "dark skin" "CIE XYZ" colourspace tristimulus '
             'values:\n'
             '\t{0}\n'
             '\n"sRGB" colourspace values (OETF):\n'
             '\n\t{1}\n'
             '\n"sRGB" and "Adobe RGB 1998" colourspaces (Linear):\n'
             '\tsRGB: {2}\n\tAdobe RGB 1998: {3}').format(
    XYZ_r1,
    np.around(sRGB_rd1 * 255),
    sRGB_r1,
    adobe98_r1))

# Preparing *green* colour in various colourspaces.
index, name, x, y, Y = data[13]
XYZ_r2 = colour.xyY_to_XYZ((x, y, Y))

# *sRGB* colourspace 0-255 values with OETF applied.
sRGB_rd2 = colour.XYZ_to_sRGB(XYZ_r2, illuminant)

# *sRGB* colourspace linear values.
sRGB_r2 = colour.XYZ_to_RGB(XYZ_r2,
                            illuminant,
                            sRGB_w,
                            sRGB_XYZ_to_RGB)

# *Adobe RGB 1998* colourspace linear values.
adobe98_r2 = colour.XYZ_to_RGB(XYZ_r2,
                               illuminant,
                               adobe98_w,
                               adobe98_XYZ_to_RGB)

message_box(('Reference "green" "CIE XYZ" colourspace tristimulus '
             'values:\n'
             '\t{0}\n'
             '\n"sRGB" colourspace values (OETF):\n'
             '\t{1}\n'
             '\n"sRGB" and "Adobe RGB 1998" colourspaces (Linear):\n'
             '\tsRGB: {2}\n\tAdobe RGB 1998: {3}').format(
    XYZ_r2,
    np.around(sRGB_rd2 * 255),
    sRGB_r2,
    adobe98_r2))
```

    ===============================================================================
    *                                                                             *
    *   Reference "dark skin" "CIE XYZ" colourspace tristimulus values:           *
    *           [ 0.11518475  0.1008      0.05089373]                             *
    *                                                                             *
    *   "sRGB" colourspace values (OETF):                                         *
    *                                                                             *
    *           [ 115.   81.   68.]                                               *
    *                                                                             *
    *   "sRGB" and "Adobe RGB 1998" colourspaces (Linear):                        *
    *           sRGB: [ 0.172906    0.08205715  0.05711951]                       *
    *           Adobe RGB 1998: [ 0.14702493  0.08205715  0.05814617]             *
    *                                                                             *
    ===============================================================================
    ===============================================================================
    *                                                                             *
    *   Reference "green" "CIE XYZ" colourspace tristimulus values:               *
    *           [ 0.14985004  0.2318      0.07900179]                             *
    *                                                                             *
    *   "sRGB" colourspace values (OETF):                                         *
    *           [  66.  149.   76.]                                               *
    *                                                                             *
    *   "sRGB" and "Adobe RGB 1998" colourspaces (Linear):                        *
    *           sRGB: [ 0.05440562  0.29876767  0.07183236]                       *
    *           Adobe RGB 1998: [ 0.12401962  0.29876767  0.08117514]             *
    *                                                                             *
    ===============================================================================

    True

Everything is setup and we are ready to apply some transformations but first, as a sanity check we ensure that *dark skin* properly converts back to *CIE XYZ* tristimulus values:

```python
XYZ_sRGB1 = colour.RGB_to_XYZ(sRGB_r1,
                              sRGB_w,
                              illuminant,
                              sRGB_RGB_to_XYZ)

XYZ_adobe981 = colour.RGB_to_XYZ(adobe98_r1,
                                 adobe98_w,
                                 illuminant,
                                 adobe98_RGB_to_XYZ)

message_box(('Converting back "dark skin" "CIE XYZ" colourspace '
             'tristimulus values from "sRGB" and "Adobe RGB 1998" '
             'colourspaces:\n'
             '\tFrom sRGB: {0}\n\tFrom Adobe RGB 1998: {1}\n'
             '\nEverything looks fine!').format(
    XYZ_sRGB1, XYZ_adobe981))
```

    ===============================================================================
    *                                                                             *
    *   Converting back "dark skin" "CIE XYZ" colourspace tristimulus values      *
    *   from "sRGB" and "Adobe RGB 1998" colourspaces:                            *
    *           From sRGB: [ 0.11518475  0.1008      0.05089373]                  *
    *           From Adobe RGB 1998: [ 0.11518475  0.1008      0.05089373]        *
    *                                                                             *
    *   Everything looks fine!                                                    *
    *                                                                             *
    ===============================================================================

    True

We multiply *dark skin* colour by *green* $n$ times and scale the result to obtain displayable values:

$$
\begin{equation}
\begin{aligned}
sRGB_m&= sRGB_{r1}\cdot sRGB_{r2}\cdot sRGB_{r2}\cdot sRGB_{r2}\cdot sRGB_k\\
adobe98_m&= adobe98_{r1}\cdot adobe98_{r2}\cdot adobe98_{r2}\cdot adobe98_{r2}\cdot adobe98_k
\end{aligned}
\end{equation}
$$

Where

$$
\begin{equation}
\begin{aligned}
sRGB_{r1}&=\ 'dark\ skin'\\
sRGB_{r2}&=\ 'green'\\
adobe98_{r1}&=\ 'dark\ skin'\\
adobe98_{r2}&=\ 'green'
\end{aligned}
\end{equation}
$$

With $sRGB_k$ and $adobe98_k$ being equivalent arbitrary scaling factors for each colourspace.

We then convert everything back to *CIE XYZ* colourspace:

```python
k = np.array([500, 500, 500])

sRGB_k = colour.XYZ_to_RGB(k,
                           illuminant,
                           sRGB_w,
                           sRGB_XYZ_to_RGB)
adobe98_k = colour.XYZ_to_RGB(k,
                              illuminant,
                              adobe98_w,
                              adobe98_XYZ_to_RGB)

sRGB_m = sRGB_r1 * sRGB_r2 * sRGB_r2 * sRGB_r2 * sRGB_k
adobe98_m = adobe98_r1 * adobe98_r2 * adobe98_r2 * adobe98_r2 * adobe98_k

XYZ_sRGB_m1 = colour.RGB_to_XYZ(sRGB_m,
                                sRGB_w,
                                sRGB_w,
                                sRGB_RGB_to_XYZ)

XYZ_adobe98_m1 = colour.RGB_to_XYZ(adobe98_m,
                                   adobe98_w,
                                   adobe98_w,
                                   adobe98_RGB_to_XYZ)

message_box(('Multiplying "dark skin" with "green" and converting back to '
             '"CIE XYZ" colourspace tristimulus values from "sRGB" and '
             '"Adobe RGB 1998" colourspaces:\n'
             '\tFrom sRGB: {0}\n\tFrom Adobe RGB 1998: {1}\n'
             '\nHouston? We have a problem!').format(
    XYZ_sRGB_m1, XYZ_adobe98_m1))
```

    ===============================================================================
    *                                                                             *
    *   Multiplying "dark skin" with "green" and converting back to "CIE XYZ"     *
    *   colourspace tristimulus values from "sRGB" and "Adobe RGB 1998"           *
    *   colourspaces:                                                             *
    *           From sRGB: [ 0.38869578  0.76482524  0.13960073]                  *
    *           From Adobe RGB 1998: [ 0.28287392  0.71107141  0.0980476 ]        *
    *                                                                             *
    *   Houston? We have a problem!                                               *
    *                                                                             *
    ===============================================================================

    True

The *CIE XYZ* colourspace tristimulus values are not equal!

The consequence of this is that if two renders are done while using the same dataset in two different colourspaces, they will not yield the same image.

In order to visualise the discrepancy, the difference between the two *dark_skin* colours lit by the *green* light is plotted:

```python
from colour.plotting import *

sRGB_m1 = colour.XYZ_to_sRGB(XYZ_sRGB_m1)
adobe98_m1 = colour.XYZ_to_sRGB(XYZ_adobe98_m1)

sRGB_difference = sRGB_m1 - adobe98_m1

# The difference is producing negative values thus we clip the result, although the
# remaining data should illustrate the effect.
sRGB_difference = np.clip(sRGB_difference, 0, 1)

single_colour_plot(colour_parameter('sRGB - Adobe RGB 1998', sRGB_difference), text_size=24)
```

The chromaticity coordinates are also out of the boundaries of their respective colourspaces:

```python
colourspaces_CIE_1931_chromaticity_diagram_plot(
    ['sRGB', 'Adobe RGB 1998'],
standalone=False,
title='"dark skin" Colour Computation')

for name, XYZ in (('"dark skin"', XYZ_r1),
                  ('"dark skin" * "green" - sRGB', XYZ_sRGB_m1),
                  ('"dark skin" * "green" - Adobe RGB 1998', XYZ_adobe98_m1)):

    xy = colour.XYZ_to_xy(XYZ)

    pylab.plot(xy[0], xy[1], 'o', color='white')
    pylab.annotate(name,
                   xy=xy,
                   xytext=(50, 30),
                   textcoords='offset points',
                   arrowprops=dict(arrowstyle='->', connectionstyle='arc3, rad=0.2'))

display(standalone=True)
```

Finally, we mathematically quantify the difference by calculating the colour difference $\Delta_{E_{ab}}$:

```python
Lab1 = colour.XYZ_to_Lab(XYZ_sRGB_m1, illuminant)
Lab2 = colour.XYZ_to_Lab(XYZ_adobe98_m1, illuminant)

print(colour.delta_E_CIE2000(Lab1, Lab2))
```

    4.90614603088

This more than two times the just noticable threshold ($JND=2.3$).

## With ACES RGB...

We will do the same computations but this time comparing *sRGB* colourspace with *ACES RGB* colourspace using the whole colour rendition chart:

```python
from pprint import pprint

aces_w = colour.ACES_RGB_COLOURSPACE.whitepoint
aces_XYZ_to_RGB = colour.ACES_RGB_COLOURSPACE.XYZ_to_RGB_matrix
aces_RGB_to_XYZ = colour.ACES_RGB_COLOURSPACE.RGB_to_XYZ_matrix

aces_r2 = colour.XYZ_to_RGB(XYZ_r2,
                            illuminant,
                            aces_w,
                            aces_XYZ_to_RGB)
aces_k = colour.XYZ_to_RGB(k,
                           illuminant,
                           aces_w,
                           aces_XYZ_to_RGB)

XYZs_m = []
for index, name, x, y, Y in data:
    xyY = np.array([x, y, Y])
    sRGB_r1 = colour.XYZ_to_RGB(
        colour.xyY_to_XYZ(xyY),
        illuminant,
        sRGB_w,
        sRGB_XYZ_to_RGB)
    sRGB_m = sRGB_r1 * sRGB_r2 * sRGB_r2 * sRGB_r2 * sRGB_k

    aces_r1 = colour.XYZ_to_RGB(
        colour.xyY_to_XYZ(xyY),
        illuminant,
        aces_w,
        aces_XYZ_to_RGB)
    aces_m = aces_r1 * aces_r2 * aces_r2 * aces_r2 * aces_k

    XYZ_sRGB_m1 = colour.RGB_to_XYZ(sRGB_m,
                                    sRGB_w,
                                    sRGB_w,
                                    sRGB_RGB_to_XYZ)
    XYZ_aces_m1 = colour.RGB_to_XYZ(aces_m,
                                    aces_w,
                                    aces_w,
                                    aces_RGB_to_XYZ)
    XYZs_m.append((XYZ_sRGB_m1, XYZ_aces_m1))

pprint(XYZs_m)
```

    [(array([ 0.38869578,  0.76482524,  0.13960073]),
      array([ 0.20268126,  0.60365236,  0.03364468])),
     (array([ 1.38456752,  2.72573459,  0.50101318]),
      array([ 0.69072112,  2.12275751,  0.12698879])),
     (array([ 0.92852793,  1.82982909,  0.37612784]),
      array([ 0.30939555,  1.31947147,  0.16922992])),
     (array([ 0.69359492,  1.3785311 ,  0.24114326]),
      array([ 0.19083716,  0.88582441,  0.0350462 ])),
     (array([ 1.03144188,  2.02284727,  0.43022108]),
      array([ 0.44387339,  1.58908042,  0.22006831])),
     (array([ 2.40152185,  4.77004663,  0.88445106]),
      array([ 0.54698139,  3.01866894,  0.22818611])),
     (array([ 0.95490787,  1.86993573,  0.31732109]),
      array([ 0.70738428,  1.66657639,  0.03354826])),
     (array([ 0.51278009,  0.99598882,  0.25222686]),
      array([ 0.23696274,  0.86242913,  0.19645941])),
     (array([ 0.41771695,  0.79872923,  0.15820317]),
      array([ 0.52233613,  0.98114033,  0.06715399])),
     (array([ 0.20837929,  0.40166426,  0.09743945]),
      array([ 0.1561374 ,  0.40618319,  0.07091007])),
     (array([ 2.3611088 ,  4.70114725,  0.79512437]),
      array([ 0.61150339,  2.93819638,  0.05968195])),
     (array([ 1.6879813 ,  3.33409446,  0.56013065]),
      array([ 0.84523956,  2.51821819,  0.04078637])),
     (array([ 0.25092775,  0.4816829 ,  0.14274428]),
      array([ 0.13600667,  0.47098151,  0.14048398])),
     (array([ 1.38992747,  2.77231474,  0.47752416]),
      array([ 0.26078266,  1.63883043,  0.05233632])),
     (array([ 0.15986689,  0.29388405,  0.05722255]),
      array([ 0.37851941,  0.55876612,  0.02558479])),
     (array([ 2.64925686,  5.25426029,  0.87775234]),
      array([ 1.02529913,  3.63311038,  0.04850628])),
     (array([ 0.44848756,  0.84955542,  0.20680862]),
      array([ 0.5579247 ,  1.10363437,  0.15522728])),
     (array([ 1.16289887,  2.30114648,  0.46972151]),
      array([ 0.25719247,  1.51339265,  0.20353625])),
     (array([ 4.31014492,  8.51455551,  1.61131629]),
      array([ 1.56833336,  6.0699454 ,  0.48769307])),
     (array([ 2.78914109,  5.50938196,  1.04588173]),
      array([ 1.01162598,  3.92908105,  0.3226246 ])),
     (array([ 1.7213125 ,  3.40015924,  0.64576132]),
      array([ 0.62258501,  2.42342331,  0.19970586])),
     (array([ 0.90371356,  1.78493521,  0.33901966]),
      array([ 0.32971348,  1.27530931,  0.10493392])),
     (array([ 0.41892695,  0.82737264,  0.15780297]),
      array([ 0.15157743,  0.59078427,  0.05004748])),
     (array([ 0.14698068,  0.29023823,  0.05542667]),
      array([ 0.05363832,  0.20784694,  0.01771646]))]

We can then compute the colour difference $\Delta_{E_{ab}}$ for each sample:

```python
deltas_E = []
for i, (XYZ1, XYZ2) in enumerate(XYZs_m):
    Lab1 = colour.XYZ_to_Lab(XYZ1, illuminant)
    Lab2 = colour.XYZ_to_Lab(XYZ2, illuminant)
    deltas_E.append((data[i][1], colour.delta_E_CIE2000(Lab1, Lab2)))

pprint(deltas_E)
```

    [(u'dark skin', 8.3613623420756102),
     (u'light skin', 8.9556903598189113),
     (u'blue sky', 13.432983122505807),
     (u'foliage', 14.044848895924677),
     (u'blue flower', 11.093053002998042),
     (u'bluish green', 16.406382114232013),
     (u'orange', 9.6603921006793581),
     (u'purplish blue', 11.67133903883059),
     (u'moderate red', 10.023064455433264),
     (u'purple', 5.9334057826699187),
     (u'yellow green', 14.805099712548397),
     (u'orange yellow', 10.551947850196745),
     (u'blue', 11.278210364179239),
     (u'green', 17.082603183950241),
     (u'red', 21.33752766059369),
     (u'yellow', 12.190174742826901),
     (u'magenta', 8.2445114939752369),
     (u'cyan', 17.17140261974528),
     (u'white 9.5 (.05 D)', 12.371645422300608),
     (u'neutral 8 (.23 D)', 12.322635558756119),
     (u'neutral 6.5 (.44 D)', 12.222855188415945),
     (u'neutral 5 (.70 D)', 11.938433138820654),
     (u'neutral 3.5 (1.05 D)', 11.766891985163152),
     (u'black 2 (1.5 D)', 11.295535127310735)]

As you can see, depending the samples colour difference $\Delta_{E_{ab}}$ can reach dramatic values. The average colour difference on the whole colour rendition chart is as follows:

```python
np.average([delta_E[1] for delta_E in deltas_E])
```

    12.256749802664634

## With Rec. 2020...

Again, the same computations but this time comparing *sRGB* colourspace with *Rec. 2020* colourspace.

```python
from pprint import pprint

rec2020_w = colour.REC_2020_COLOURSPACE.whitepoint
rec2020_XYZ_to_RGB = colour.REC_2020_COLOURSPACE.XYZ_to_RGB_matrix
rec2020_RGB_to_XYZ = colour.REC_2020_COLOURSPACE.RGB_to_XYZ_matrix

rec2020_r2 = colour.XYZ_to_RGB(XYZ_r2,
                               illuminant,
                               rec2020_w,
                               rec2020_XYZ_to_RGB)
rec2020_k = colour.XYZ_to_RGB(k,
                              illuminant,
                              aces_w,
                              aces_XYZ_to_RGB)
XYZs_m = []
for index, name, x, y, Y in data:
    xyY = np.array([x, y, Y])
    sRGB_r1 = colour.XYZ_to_RGB(
        colour.xyY_to_XYZ(xyY),
        illuminant,
        sRGB_w,
        sRGB_XYZ_to_RGB)
    sRGB_m = sRGB_r1 * sRGB_r2 * sRGB_r2 * sRGB_r2 * sRGB_k

    rec2020_r1 = colour.XYZ_to_RGB(
        colour.xyY_to_XYZ(xyY),
        illuminant,
        rec2020_w,
        rec2020_XYZ_to_RGB)
    rec2020_m = rec2020_r1 * rec2020_r2 * rec2020_r2 * rec2020_r2 * rec2020_k

    XYZ_sRGB_m1 = colour.RGB_to_XYZ(sRGB_m,
                                    sRGB_w,
                                    sRGB_w,
                                    sRGB_RGB_to_XYZ)
    XYZ_rec2020_m1 = colour.RGB_to_XYZ(rec2020_m,
                                       rec2020_w,
                                       rec2020_w,
                                       rec2020_RGB_to_XYZ)
    XYZs_m.append((XYZ_sRGB_m1, XYZ_rec2020_m1))

pprint(XYZs_m)
```

    [(array([ 0.38869578,  0.76482524,  0.13960073]),
      array([ 0.25853752,  0.70148175,  0.05716381])),
     (array([ 1.38456752,  2.72573459,  0.50101318]),
      array([ 0.89307458,  2.46736894,  0.20938258])),
     (array([ 0.92852793,  1.82982909,  0.37612784]),
      array([ 0.4476039 ,  1.47882622,  0.21349428])),
     (array([ 0.69359492,  1.3785311 ,  0.24114326]),
      array([ 0.32760172,  1.10831102,  0.07492719])),
     (array([ 1.03144188,  2.02284727,  0.43022108]),
      array([ 0.57059404,  1.71920717,  0.26897265])),
     (array([ 2.40152185,  4.77004663,  0.88445106]),
      array([ 1.00942471,  3.68779206,  0.35432283])),
     (array([ 0.95490787,  1.86993573,  0.31732109]),
      array([ 0.82224442,  1.93413951,  0.10070503])),
     (array([ 0.51278009,  0.99598882,  0.25222686]),
      array([ 0.27187771,  0.83126526,  0.21373168])),
     (array([ 0.41771695,  0.79872923,  0.15820317]),
      array([ 0.52016768,  1.01546347,  0.09730777])),
     (array([ 0.20837929,  0.40166426,  0.09743945]),
      array([ 0.16245725,  0.3957943 ,  0.08017173])),
     (array([ 2.3611088 ,  4.70114725,  0.79512437]),
      array([ 1.10102029,  3.76453958,  0.19959814])),
     (array([ 1.6879813 ,  3.33409446,  0.56013065]),
      array([ 1.12444424,  3.06090261,  0.15106385])),
     (array([ 0.25092775,  0.4816829 ,  0.14274428]),
      array([ 0.13673238,  0.40545703,  0.14564003])),
     (array([ 1.38992747,  2.77231474,  0.47752416]),
      array([ 0.56129417,  2.11891082,  0.13077818])),
     (array([ 0.15986689,  0.29388405,  0.05722255]),
      array([ 0.34651438,  0.55009629,  0.04155109])),
     (array([ 2.64925686,  5.25426029,  0.87775234]),
      array([ 1.52064548,  4.53855693,  0.21546166])),
     (array([ 0.44848756,  0.84955542,  0.20680862]),
      array([ 0.53200208,  1.05121778,  0.18042333])),
     (array([ 1.16289887,  2.30114648,  0.46972151]),
      array([ 0.45549085,  1.73803537,  0.25647096])),
     (array([ 4.31014492,  8.51455551,  1.61131629]),
      array([ 2.27314638,  7.11456531,  0.72189921])),
     (array([ 2.78914109,  5.50938196,  1.04588173]),
      array([ 1.46624858,  4.59788321,  0.47349996])),
     (array([ 1.7213125 ,  3.40015924,  0.64576132]),
      array([ 0.90328444,  2.83574094,  0.29272411])),
     (array([ 0.90371356,  1.78493521,  0.33901966]),
      array([ 0.47656235,  1.49134012,  0.15383292])),
     (array([ 0.41892695,  0.82737264,  0.15780297]),
      array([ 0.21946491,  0.68956933,  0.07256384])),
     (array([ 0.14698068,  0.29023823,  0.05542667]),
      array([ 0.07733116,  0.24227999,  0.0256142 ]))]

We again compute the colour difference $\Delta_E$:

```python
deltas_E = []
for i, (XYZ1, XYZ2) in enumerate(XYZs_m):
    Lab1 = colour.XYZ_to_Lab(XYZ1, illuminant)
    Lab2 = colour.XYZ_to_Lab(XYZ2, illuminant)
    deltas_E.append((data[i][1], colour.delta_E_CIE2000(Lab1, Lab2, l=1)))

pprint(deltas_E)
```

    [(u'dark skin', 6.186862353654381),
     (u'light skin', 6.6841065817906804),
     (u'blue sky', 9.9232992371789344),
     (u'foliage', 9.3146056130510342),
     (u'blue flower', 8.5710337656737838),
     (u'bluish green', 11.091417794697131),
     (u'orange', 6.856998379754125),
     (u'purplish blue', 9.9593838336339058),
     (u'moderate red', 8.4147777495143163),
     (u'purple', 4.7878515344863057),
     (u'yellow green', 9.7069670435799722),
     (u'orange yellow', 7.1359690495715338),
     (u'blue', 10.297704619280728),
     (u'green', 10.948839961415668),
     (u'red', 17.995992699335542),
     (u'yellow', 8.0907740434874338),
     (u'magenta', 6.2573732490286149),
     (u'cyan', 12.272493214747699),
     (u'white 9.5 (.05 D)', 8.8933966206553361),
     (u'neutral 8 (.23 D)', 8.8473904596447657),
     (u'neutral 6.5 (.44 D)', 8.7498743874243026),
     (u'neutral 5 (.70 D)', 8.5177561630589675),
     (u'neutral 3.5 (1.05 D)', 8.3485958287626083),
     (u'black 2 (1.5 D)', 7.9149962914638188)]

Finally, we compute the average colour difference:

```python
np.average(np.average([delta_E[1] for delta_E in deltas_E]))
```

    8.9903525197871481

## Analytical Explanation

Converting from a colourspace $RGB_1$ to another colourspace $RGB_2$ is done by applying a series of linear transformations (matrices) as follows:

$$
\begin{equation}
\begin{vmatrix}
R_2\\
G_2\\
B_2
\end{vmatrix}=
\textbf{M}_{RGB_2}\cdot \textbf{M}_{XYZ_1}
\begin{vmatrix}
R_1\\
G_1\\
B_1
\end{vmatrix}
\end{equation}
$$

where $\textbf{M}_{RGB_2}$ is the $3x3$ matrix transforming from *CIE XYZ* colourspace to $RGB_2$ colourspace and $\textbf{M}_{XYZ_1}$ is the $3x3$ matrix transforming from $RGB_1$ colourspace to *CIE XYZ* colourspace.

> Note: We have discounted chromatic adaptation to makes things easier.

Here are such $\textbf{M}_{XYZ_1}$ matrices for respectively *sRGB* and *ACES RGB* colourspaces:

$$
\begin{equation}
\textbf{M}_{XYZ_{sRGB}}=
\begin{vmatrix}
0.41238656&0.35759149&0.18045049\\
0.21263682&0.71518298&0.0721802\\
0.01933062&0.11919716&0.95037259\\
\end{vmatrix}\\
\textbf{M}_{XYZ_{ACES}}=
\begin{vmatrix}
0.9525524&0&0.00009368\\
0.34396645&0.7281661&-0.07213255\\
0&0&1.00882518\\
\end{vmatrix}\\
\end{equation}
$$

Matrices are really just a system of multiple linear equations, thus we can express $\textbf{M}_{XYZ_{sRGB}}$ as follows:

$$
\begin{equation}
\begin{aligned}
X&=\ 0.41238656R+0.35759149G+0.18045049B\\
Y&=\ 0.21263682R+0.71518298G+0.0721802B\\
Z&=\ 0.01933062R+0.11919716G+0.95037259B\\
\end{aligned}
\end{equation}
$$

We will simplify the problem again by just considering the first equation of each system, the first row of each matrix:

$$
\begin{equation}
\begin{aligned}
X_{sRGB}&=\ 0.41238656R+0.35759149G+0.18045049B\\
X_{ACES}&=\ 0.9525524R+0.0G+0.00009368B\\
\end{aligned}
\end{equation}
$$

We plot the functions by using a uniformly spaced incrementing $k$ variable in place of $RGB$:

```python
k = np.linspace(0, 1, 10)
X_sRGB = lambda x: 0.41238656 * x + 0.35759149 * x + 0.18045049 * x
X_ACES = lambda x: 0.9525524 * x + 0.00009368 * x

pylab.plot(k, tuple(map(X_sRGB, k)), 'o-', color='red', label='X - sRGB')
pylab.plot(k, tuple(map(X_ACES, k)), 'o-', color='green', label='X - ACES RGB')

settings = {'title': 'sRGB - ACES RGB - Uniformly Spaced Incrementing RGB',
            'x_label': 'k',
            'y_label': 'X',
            'x_tighten': True,
            'legend': True,
            'legend_location': 'upper left',
            'x_ticker': True,
            'y_ticker': True}
decorate(**settings)
display(**settings)
```

They match each other very well, which is the expected and correct behavior, now we re-plot the functions while only incrementing the $R$ component:

```python
k = np.linspace(0, 1, 10)
X_sRGB = lambda x: 0.41238656 * x
X_ACES = lambda x: 0.9525524 * x

pylab.plot(k, tuple(map(X_sRGB, k)), 'o-', color='red', label='X - sRGB')
pylab.plot(k, tuple(map(X_ACES, k)), 'o-', color='green', label='X - ACES RGB')

settings = {'title': 'sRGB - ACES RGB - Uniformly Spaced Incrementing R',
            'x_label': 'k',
            'y_label': 'X',
            'x_tighten': True,
            'legend': True,
            'legend_location': 'upper left',
            'x_ticker': True,
            'y_ticker': True}
decorate(**settings)
display(**settings)
```

The slopes are obviously very different now, *ACES RGB* is steeper than *sRGB*. This is the result of the respective colourspaces basis vectors not being orthogonal. In this case it leads to colours approaching the spectral locus boundaries in *ACES RGB* faster than *sRGB*. It is the reason why in some instances secondary bounces quickly lead to very saturated colours.

This can be again demonstrated by multiplying our *dark skin* and *green* colours from above computations and plotting them:

```python
pylab.plot(X_sRGB(0.172906) * X_sRGB(0.05440562), 0, 'o', color='red', label='X - "dark skin" * "green" sRGB')
pylab.plot(X_ACES(0.11758989) * X_ACES(0.15129818), 0, 'o', color='green', label='X - "dark skin" * "green" ACES RGB')

settings = {'title': 'sRGB - ACES RGB - X Value - "dark skin" * "green"',
            'x_label': 'X Value',
            'x_tighten': True,
            'legend': True,
            'legend_location': 'upper left',
            'x_ticker': True,
            'y_ticker': True}
decorate(**settings)
display(**settings)
```

## Conclusion

```python
colourspaces_CIE_1931_chromaticity_diagram_plot(
    ['Pointer Gamut', 'sRGB', 'Rec. 2020', 'ACES RGB'])
```

While most of this is tied to basic linear algebra, it has been overlooked by most of VFX industry veterans. We were assuming wrongly that rendering engines are colourspace agnostic.

On a strictly technical point of view, rendering engines are indeed colourspace agnostic. They just chew through whatever data you throw at them without making any consideration of the colourspace the data is stored into.

However the choice of colourspace and its primaries is critical to achieve a faithful rendering. In rendering context, wide gamut colour spaces are prone to create colours with very high chroma and this can be a major issue when trying to achieve photo-realistic output.

The root cause of those issues is linked to the current $RGB$ lighting model that *most* rendering engines use. There are a lot of practical reasons to use the $RGB$ lighting model instead of a full spectral model, especially when our data representation is based on a $RGB$ triplet. It is indeed very hard to measure reflectance data in large volumes and with enough control and resolution for production work. Applications to manipulate that reflectance data don't exist or are tools dedicated to scientists, thus not really useful for content creation. As of today, it is impossible to launch *Spectral Photoshop* in order to apply a *Spectral Gaussian Blur* on a *Spectral Textures* dataset.

Plates and photographs are often shot with a lighting setup that doesn't match any known illuminant or worse, mixes multiple light sources with different relative spectral power distributions. HDRI images used for lighting are no exception and cannot convey properly the illumination of a scene.

We may be often tempted to think of photographs as *true* reflectance data, but they are actually reflected spectra: They have already been multiplied by the spectra of multiple different light sources and by the reflected spectra from neighbour surfaces. They should not be assimilated to quantities meaningful in really accurate colorimetry computations.

Another issue is that artists are able to specify colours from the whole current colourspace volume without accounting for the fact that their chromaticities can be outside the boundary of any real object colour gamuts like Pointer's Gamut, ISO 12640-3 or Xun Li's Gamut, thus almost systematically yielding implausible colours. It seems a necessity to have tools allowing to check that:

* Picked colours are within the volume of a real object colours gamut (Pointer's Gamut for instance).
* At any moment, displayed colours on a device are within the volume of a real object colours gamut.
* Existing colours in a texture or a photo are within the volume of a real object colours gamut.

The $RGB$ lighting model doesn't allow precise colorimetric value computations but we still can produce images faithful and pleasing thanks to our artistic sense. We have been doing that for years and this is probably the most important thing for the entertainment industry.

Ultimately, the solution will be the switch to spectral rendering, it will happen. Question is when? There are already quite a lot of spectral renderers outside. Weta Digital has been using [Manuka](http://www.fxguide.com/featured/manuka-weta-digitals-new-renderer/) on a few movies already. We still need to address the issues with reflectance data before they become mainstream. While its theoretically impossible to reconstruct a spectral power distribution from a single $RGB$ triplet because an infinity of spectral power distributions yield to the same tristimulus values, some research has been done on the problem and a few methods exists. For example, *Brian Smits (1999)* describes a method that leads to [very good results](http://nbviewer.jupyter.org/github/colour-science/colour-website/blob/master/ipython/about_reflectance_recovery.ipynb). The recovered reflectance data is not useful for precise colorimetry but it yields very good results in the context of computer graphics imagery.

As of now, we continue discussing what colourspace is the best for computations in the $RGB$ lighting model and the issue has also been raised on the [ACES mailing list](https://groups.google.com/forum/#!topic/academyaces/9b4VuqPcOHQ).

## November 28, 2014 - Erratum

Elle Stone sent me a mail with various questions regarding the document including her concerns that the chromaticities in the computations were falling outside the respective colourspace gamut in the *"dark skin" - Colour Computation* figure.

This is indeed an issue and should never happen in the context of those computations. Given positive *RGB* values and accurate conversion to *xy* chromaticities coordinates, the latter should be located within the boundaries of the reference colourspace.

I tracked down the problem to an erroneous whitepoint adaptation: As an early sanity check, we were converting back to *CIE XYZ* tristimulus values, ensuring the API was working correctly:

```python
XYZ_sRGB1 = colour.RGB_to_XYZ(sRGB_r1,
                              sRGB_w,
                              illuminant,
                              sRGB_RGB_to_XYZ)
```

Unfortunately the same code was also used for the tristimulus values conversion post-multiplication operation:

```python
XYZ_sRGB_m1 = colour.RGB_to_XYZ(sRGB_m,
                                sRGB_w,
                                illuminant,
                                sRGB_RGB_to_XYZ)
```

As a result, we were chromatically adapting to *CIE Illuminant C* instead of the *CIE Illuminant D Series D65*.
In the current context, the correct conversion to tristimulus values should entirely bypass chromatic adaptation since the source and target whitepoints are the same.

Incorrect instances of the code have been fixed and figures updated accordingly.


# Linear diffusion exercise with Landlab

This notebook is adapted from *Landscape Evolution Modeling with CHILD* by Gregory Tucker and Stephen Lancaster. This notebook was created by Nicole Gasparini at Tulane University.

For tutorials on learning Landlab, click here: https://github.com/landlab/landlab/wiki/Tutorials

**What is this notebook?**

This notebook illustrates the evolution of landforms dominated by processes that result in linear diffusion of sediment. In other words, the downhill flow of soil is proportional to the (downhill) gradient of the land surface multiplied by a transport coefficient.

The notebook first illustrates a simple example of a diffusing hillslope. We then provide a number of exercises for students to do on their own. This set of exercises is recomended for students in a quantitative geomorphology class, who have been introduced to the linear diffusion equation in class.

**Application of linear diffusion transport law:**

For relatively gentle, soil-mantled slopes, there is reasonably strong support for a transport law of the form:
\begin{equation}
q_s = -D \nabla z
\end{equation}
where ${q}_s$ is the transport rate with dimensions of L$^2$T$^{-1}$;  $D$ is a transport coefficient with dimensions of L$^2$T$^{-1}$; and $z$ is elevation. $\nabla z$ is the gradient in elevation. If distance is increasing downslope, $\nabla z$ is negative downslope, hence the negative in front of $D$.

Changes in elevation, or erosion, are calculated from conservation of mass:
\begin{equation}
\frac{dz}{dt} = U-\nabla q_s
\end{equation}
where $U$ is the rock uplift rate, with dimensions LT$^{-1}$.

**How will we explore this with Landlab?**

We will use the Landlab component *LinearDiffuser*, which implements the equations above, to explore how hillslopes evolve when linear diffusion describes hillslope sediment transport. We will explore both steady state, here defined as erosion rate equal to rock uplift rate, and also how a landscape gets to steady state.

The first example illustrates how to set-up the model and evolve a hillslope to steady state, along with how to plot some variables of interest. We assume that you have knowledge of how to derive the steady-state form of a uniformly uplifting, steady-state, diffusive hillslope. For more information on hillslope sediment transport laws, this paper is a great overview:

Roering, Joshua J. (2008) "How well can hillslope evolution models “explain” topography? Simulating soil transport and production with high-resolution topographic data." Geological Society of America Bulletin.

Based on the first example, you are asked to first think about what will happen as you change a parameter, and then you explore this numerically by changing the code.

Start at the top by reading each block of text and sequentially running each code block (shift - enter OR got to the _Cell_ pulldown menu at the top and choose _Run Cells_).

Remember that you can always go to the _Kernel_ pulldown menu at the top and choose _Restart & Clear Output_ or _Restart & Run All_ if you change things and want to start afresh. If you just change one code block and rerun only that code block, only the parts of the code in that code block will be updated. (E.g. if you change parameters but don't reset the code blocks that initialize run time or topography, then these values will not be reset.)

**Now on to the code example**

Import statements. You should not need to edit this.

```python
# below is to make plots show up in the notebook
%matplotlib inline
```

```python
# Code Block 1

import numpy as np
from matplotlib.pyplot import figure, legend, plot, show, title, xlabel, ylabel, ylim

from landlab.plot.imshow import imshow_grid
```

We will create a grid with 41 rows and 5 columns, and dx is 5 m (a long, narrow, hillslope). The initial elevation is 0 at all nodes.

We set-up boundary conditions so that material can leave the hillslope at the two short ends.

```python
# Code Block 2

# setup grid
from landlab import RasterModelGrid

mg = RasterModelGrid((41, 5), 5.0)
z_vals = mg.add_zeros("topographic__elevation", at="node")

# initialize some values for plotting
ycoord_rast = mg.node_vector_to_raster(mg.node_y)
ys_grid = ycoord_rast[:, 2]

# set boundary condition.
mg.set_closed_boundaries_at_grid_edges(True, False, True, False)
```

Now we import and initialize the *LinearDiffuser* component.

```python
# Code Block 3

from landlab.components import LinearDiffuser

D = 0.01  # initial value of 0.01 m^2/yr
lin_diffuse = LinearDiffuser(mg, linear_diffusivity=D)
```

We now initialize a few more parameters.

```python
# Code Block 4

# Uniform rate of rock uplift
uplift_rate = 0.0001  # meters/year, originally set to 0.0001

# Total time in years that the model will run for.
runtime = 1000000  # years, originally set to 1,000,000

# Stability criteria for timestep dt.  Coefficient can be changed
# depending on our tolerance for stability vs tolerance for run time.
dt = 0.5 * mg.dx * mg.dx / D

# nt is number of time steps
nt = int(runtime // dt)

# Below is to keep track of time for labeling plots
time_counter = 0

# length of uplift over a single time step, meters
uplift_per_step = uplift_rate * dt
```

Now we figure out the analytical solution for the elevation of the steady-state profile.

```python
# Code Block 5

ys = np.arange(mg.number_of_node_rows * mg.dx - mg.dx)

# location of divide or ridge crest -> middle of grid
# based on boundary conds.
divide_loc = (mg.number_of_node_rows * mg.dx - mg.dx) / 2

# half-width of the ridge
half_width = (mg.number_of_node_rows * mg.dx - mg.dx) / 2

# analytical solution for elevation under linear diffusion at steady state
zs = (uplift_rate / (2 * D)) * (np.power(half_width, 2) - np.power(ys - divide_loc, 2))
```

Before we evolve the landscape, let's look at the initial topography. (This is just verifying that it is flat with zero elevation.)

```python
# Code Block 6

figure(1)
imshow_grid(mg, "topographic__elevation")
title("initial topography")
figure(2)
elev_rast = mg.node_vector_to_raster(mg.at_node["topographic__elevation"])
plot(ys_grid, elev_rast[:, 2], "r-", label="model")
plot(ys, zs, "k--", label="analytical solution")
ylim((-5, 50))  # may want to change upper limit if D changes
xlabel("horizontal distance (m)")
ylabel("vertical distance (m)")
legend(loc="lower center")
title("initial topographic cross section")
```

Now we are ready to evolve the landscape and compare it to the steady state solution.

Below is the time loop that does all the calculations.

```python
# Code Block 7

for i in range(nt):
    mg["node"]["topographic__elevation"][mg.core_nodes] += uplift_per_step
    lin_diffuse.run_one_step(dt)
    time_counter += dt

    # All landscape evolution is the first two lines of loop.
    # Below is simply for plotting the topography halfway through the run
    if i == int(nt // 2):
        figure(1)
        imshow_grid(mg, "topographic__elevation")
        title("topography at time %s, with D = %s" % (time_counter, D))
        figure(2)
        elev_rast = mg.node_vector_to_raster(mg.at_node["topographic__elevation"])
        plot(ys_grid, elev_rast[:, 2], "k-", label="model")
        plot(ys, zs, "g--", label="analytical solution - SS")
        plot(ys, zs * 0.75, "b--", label="75% of analytical solution")
        plot(ys, zs * 0.5, "r--", label="50% of analytical solution")
        xlabel("horizontal distance (m)")
        ylabel("vertical distance (m)")
        legend(loc="lower center")
        title("topographic__elevation at time %s, with D = %s" % (time_counter, D))
```

Now we plot the final cross-section.

```python
# Code Block 8

elev_rast = mg.node_vector_to_raster(mg.at_node["topographic__elevation"])
plot(ys_grid, elev_rast[:, 2], "k-", label="model")
plot(ys, zs, "g--", label="analytical solution - SS")
plot(ys, zs * 0.75, "b--", label="75% of analytical solution")
plot(ys, zs * 0.5, "r--", label="50% of analytical solution")
xlabel("horizontal distance (m)")
ylabel("vertical distance (m)")
legend(loc="lower center")
title("topographic cross section at time %s, with D = %s" % (time_counter, D))
```

Now we plot the steepest slope in the downward direction across the landscape.

(To calculate the steepest slope at a location, we need to route flow across the landscape.)

```python
# Code Block 9

from landlab.components import FlowAccumulator

fr = FlowAccumulator(mg)  # intializing flow routing
fr.run_one_step()
plot(
    mg.node_y[mg.core_nodes],
    mg.at_node["topographic__steepest_slope"][mg.core_nodes],
    "k-",
)
xlabel("horizontal distance (m)")
ylabel("topographic slope (m/m)")
title("slope of the hillslope at time %s, with D = %s" % (time_counter, D))
```

Has the landscape reached steady state yet? How do you know?

Answer: Not quite, but it is getting close. Go back and rerun Code Blocks 7, 8 and 9 (time loop and plotting). (Remember you can rerun a cell with shift-return, or from the cell pull-down menu.) Has it reached steady state yet?

**What to do and hand in:**
1. In the example illustrated here ($D$ = 0.01 m$^2$yr$^{-1}$ and $U$ = 0.0001 m yr$^{-1}$). Restart everything, and use the model to determine how long it takes for the landscape to go from a flat to reach 50%, 75% and 100% of its steady-state morphology. Does the landscape approach steady state linearly in time? (You can run the time loop (Code Block 7) multiple times without running other code blocks again to continually evolve the landscape. You will initially want to rerun all the code blocks and change the value of **run_time** (Code Block 4). Determining the correct value of **run_time** to use will take some iteration.)
2. What do you think will happen when you increase $D$ (Code Block 3) by a factor of 10? Will the time to steady state differ? If yes, how? Will the topography be different? If yes, how and why? What does it mean physically, about processes, if $D$ increases? Answer these questions before running any code.
3. Now set $D$ = 0.1 m$^2$yr$^{-1}$ and rerun landscape evolution from an initial flat. Illustrate the final steady state topography and record the time to steady state. Discuss how the landscape differs from the results in question 1. Discuss how the results are similar to or different from your intuition. It is OK if your intuition was wrong!
4. What do you think will happen when you increase **uplift_rate** (Code Block 4) by a factor of 10? Will the time to steady state differ? If yes, how? Will the topography be different? If yes, how and why? Answer these questions first, and then rerun the code with **uplift_rate** = 0.001 m yr$^{-1}$. (Make sure you change $D$ - Code Block 3 - back to the original value of 0.01 m$^2$yr$^{-1}$ and restart from a flat surface.) Illustrate the final steady state topography. Discuss how these results differ from the results in question 1 and how the results match (or do not) your intuition. It is OK if your intuition was wrong.

You should hand in a typed document that answers the above questions with supporting plots. Plots should be embedded in the text, or, if they all fall at the end, they need to be clearly labeled, e.g. each plot has a figure number and plots are referred to by figure number in the text.

Other questions you can explore.

1. What happens to time to steady state as you increase the length of your hillslope?
2. Does grid resolution affect your answers? If so, how?


# Software profesional en Acústica 2020-21 (M2i)

*This notebook has been inspired from Chapter 11 - Transmission of Sound through Partitions of the book 'Foudations of Engineering Acoustics Frank Fahy, Academic Press (2001) https://doi.org/10.1016/B978-012247665-5/50012-6.*

# Introducción al método de transferencia de matrices (TMM)

Los medios multicapa son soluciones habituales para
muchos de los problemas que nos podemos encontrar en acústica.
Problemas como el control de ruido y la reducción de vibraciones
requieren en la práctica paredes o revestimientos compuestos por
una sucesión de diferentes materiales con los que controlar
la propagación de ondas acústicas. En la práctica, existen multitud
de materiales con los que construir un medio multicapa y las posibles combinaciones
con las que nos podemos enfrentar pueden incluir cámaras de aire,
materiales cerámicos, lanas de roca, láminas metálicas, velos porosos, etc.

## Objetivos

- Modelar el comportamiento acústica de un material multicapa
- Resolver la propagación armóninca de ondas planas en incidencia normal en un medio multicapa
- Calcular niveles y coeficientes acústicos: absorción, reflexión, nivel de presión sonora y pérdida por transmisión
- Representar gráficas de respuesta en frecuencia de índices asociados a fenómenos de propagación

# Propagación de ondas armónicas planas en un medio multicapa

Los medios multicapa son soluciones habituales para
muchos de los problemas que nos podemos encontrar en acústica.
Problemas como el control de ruido y la reducción de vibraciones
requieren en la práctica paredes o revestimientos compuestos por
una sucesión de diferentes materiales con los que controlar
la propagación de ondas acústicas. En la práctica, existen multitud
de materiales con los que construir un medio multicapa y las posibles combinaciones
con las que nos podemos enfrentar pueden incluir cámaras de aire,
materiales cerámicos, lanas de roca, láminas metálicas, velos porosos, etc.

De forma general, supondremos que un corte vertical de un medio multicapa formada por $n$ medios cuyas
interfaces están definidas por los planos $p=a_{j}$ para cada $j=1,\ldots,n-1$ está descrito como aparece en la siguiente figura:

En cada una de las interfaces de acople entre dos medios (capas) consecutivas se denotará por $Z_{j}^{in}$ y $Z_{j}^{out}$ a la impedancia de superficie de entrada y salida asociada a la interfaz $j$, respectivamente.

En lo que sigue y para ilustrar de una forma simple los cálculos
que se deben realizar para resolver
la propagación de ondas planas a través de un medio multicapa,
consideraremos el caso de ondas planas unidimensionales y
supondremos que los desplazamientos de cada uno de los medios son suma de dos ondas
planas, una progresiva y otra regresiva. Las ecuaciones que
satisfacen los campos de desplazamientos $u_j(p,t)$, para $j=1,2,\ldots,n$ son
\begin{equation}
\frac{\partial^2 u_{j}}{\partial t^2}-c_{j}\frac{\partial^2u_{j}}{\partial p^2}=0\qquad
\text{ en el medio }j,
\end{equation}
donde $c_{j}$ es la velocidad de propagación del medio $j$.

Puesto que no estamos interesados en la transición inicial del
campo de desplazamientos, no daremos condiciones iniciales $u_j(p,0)$
o $\frac{\partial u_j}{\partial t}(p,0)$ para cada $j=1,2,\ldots,n$.
En cambio, buscaremos soluciones armónicas donde sólo la primera componente
es no nula, es decir, el campo de desplazamientos $u_{j}$,
velocidades $v_{j}$ y presiones $\pi_j$ de la capa $j$-ésima son de la forma
\begin{align}
&u_j(p,t)=\mathrm{Re}\left(e^{-i\omega t}U_j(p)\right)\mathbf{e}_{1},\\
&v_j(p,t)=\mathrm{Re}\left(e^{-i\omega t}V_j(p)\right)\mathbf{e}_{1},\\
&\pi_j(p,t)=\mathrm{Re}\left(e^{-i\omega t}\Pi_j(p)\right),
\end{align}
para $j=1,2,\ldots,n$.

Si nuestro objetivo es simular el comportamiento acústico de la
multicapa cuando la excitamos en el medio $1$
mediante una onda plana progresiva de amplitud compleja $F$,
podemos escribir las condiciones de contorno del problema como
$$
\left.\left(\frac{d U_{1}}{d p}+ik_{1}U_{1}\right)\right|_{p=a_{1}}=2ik_{1}Fe^{ik_{1}a_{1}},
$$
junto con la condición de radiación saliente
\begin{equation}
\lim_{p\to+\infty}\left(\frac{d U_{n}}{d p}-ik_{n}U_{n}\right)=0,
\end{equation}
donde $k_j$ es el número de onda del medio $j$. La ecuación
anterior asegura que no existen ondas regresivas
en el medio $n$ situado en el extremo derecho.

En consecuencia, en el dominio de la frecuencia,
los desplazamientos complejos $U_{j}$ son solución del siguiente problema
en el que aún faltan las condiciones de transmisión:
Hallar los campos de desplazamientos complejos $U_j(p)$,
para $j=1,2,\ldots,n$ tales que satisfacen las ecuaciones
\begin{equation}
\left\{\begin{array}{rcll}
 k_{j}U_{j}+\frac{d^2U_{j}}{d p^2}&=&0&\text{ en el medio }j,\\
\lim_{p\to+\infty}\left(\frac{d U_{n}}{d p}-ik_{n}U_{n}\right)&=&0,&\\
\left.\left(\frac{d U_{1}}{d p}+ik_{1}U_{1}\right)\right|_{p=a_{1}}&=&2ik_{1}Fe^{ik_{1}a_{1}},&
\end{array}\right.
\end{equation}

## Método de transferencia de matrices
Los campos de desplazamientos complejos $U_j$ se pueden escribir como
\begin{equation}\label{eq:tiposolfunmul}
U_j(p)=C_{2j-1}e^{-ik_{j}p}+C_{2j}e^{ik_{j}p},
\qquad j=1,2,\ldots,n,
\end{equation}
donde las amplitudes $C_{2j-1}$, y $C_{2j}$ corresponden a
las ondas regresivas y progresivas respectivamente, de cada medio.
Obviamente, las amplitudes de cada una de las ondas que forman
parte del desplazamiento $U_{j}$, deben ser determinadas a partir de las condiciones
de transmisión de cada una de las interfaces de la multicapa.

Por otra parte, los diferentes tipos de interfaz considerados entre dos medios
(interfaz de contacto, placa rígida, panel viscoelástico, velo poroso, etc)
dan lugar a diferentes condiciones de transmisión,
para cada interfaz $j=1,2,\ldots,n-1$, que se traducen en dos ecuaciones lineales respecto a las
amplitudes de los desplazamientos en cada una de las interfaces.
Estas dos ecuaciones se pueden escribir matricialmente, en cada plano $p=a_j$, como
\begin{equation*}
A_j(C_{2j-1},C_{2j},C_{2j+1},C_{2j+2})^t=(0,0)^t,
\end{equation*}
siendo $A _j$ una matriz de dos filas y cuatro columnas, $A _j\in{\mathcal{M}}_{2\times 4}(\mathbb{C})$.
Cada matriz $A_j$ se puede descomponer en dos matrices $2\times 2$ para diferenciar las columnas que
multiplican a las amplitudes del medio $j$ y $j+1$,
\begin{equation*}
A_j=\left(A _{j,j}\ A_{j,j+1}\right)
\qquad \mbox{ con }A_{j,j+1},A_{j,j}\in\mathcal{M}_{2\times 2}(\mathbb{C}).
\end{equation*}
Entonces, si suponemos que la primera onda plana progresiva
tiene una amplitud $F$, debemos resolver el siguiente sistema
de $2n$ ecuaciones con $2n$ incógnitas:
\begin{equation}\label{eq:linsys}
AC=F ,
\end{equation}
donde $C=(C_1,\ldots,C_{2n})^t\in\mathbb{C}^{2n}$,
$F=(F,0,\ldots,0)^t\in\mathbb{C}^{2n}$ y
\begin{equation*}
A =
\begin{pmatrix}
A _{0,1}&                &                &                &                &                \\
A _{1,1}&A _{1,2}&                &                &                &                \\
                &A _{2,2}&A _{2,3}&                &                &                \\
            &                &A _{3,3}&A _{3,4}&                &                \\
                &                &                & \ddots         &                &                \\
                &                &                &                &A _{n-1,n-1}&A _{n-1,n}\\
                &                &                &                &                &A _{n,n}
\end{pmatrix}
\in\mathcal{M}_{2n\times 2n}(\mathbb{C}).
\end{equation*}
En este sistema  $A _{0,1}=(0\;1)\in\mathcal{M}_{1\times 2}$
y $A _{n,n}=(1\; 0)\in\mathcal{M}_{1\times 2}$.
La primera indica la existencia de una onda incidente progresiva
en el primer  medio de amplitud $F$ y la última indica que no existen
ondas regresivas en el medio $n$ (que no es más que una reinterpretación de la condición de radiación
de Sommerfeld).

## Impedancia de entrada y salida. Coeficientes de reflexión, absorción y transmisión
Al resolver el sistema lineal anterior se obtiene
el vector $C$ que tiene por componentes las amplitudes de las ondas planas
que son solución del problema de propagación acústica a través del medio multicapa.
A partir de estas amplitudes podemos calcular todas las cantidades físicas de interés
(intensidad, potencia,etc.) y los coeficientes de reflexión y transmisión entre cada capa.

Teniendo en cuenta la solución del problema lineal anterior, la solución del problema de propagación unidimensional que estamos
considerando, esto es, tanto la presión $\Pi_{j}$ como la velocidad $V_{j}$ y el desplazamiento $U_{j}$
en el medio $j$ se pueden expresar como suma de ondas progresivas y regresivas
\begin{align*}
\Pi_{j}(p)&=\Pi_{\mathrm{pro},j}(p)+\Pi_{\mathrm{reg},j}(p),\\
V_{j}(p)&=V_{\mathrm{pro},j}(p)+V_{\mathrm{reg},j}(p),\\
U_{j}(p)&=U_{\mathrm{pro},j}(p)+V_{\mathrm{reg},j}(p),
\end{align*}
donde
\begin{align*}
\Pi_{\mathrm{pro},j}(p)&=-i\omega Z_{j}C_{2j-1}e^{-ik_{j}p},\qquad
\Pi_{\mathrm{reg},j}(p)=i\omega Z_{j}C_{2j}e^{ik_{j}p},\\
V_{\mathrm{pro},j}(p)&=-i\omega U_{\mathrm{pro},j}(p)=-i\omega C_{2j-1}e^{-ik_{j}p},\qquad
V_{\mathrm{reg},j}(p)=-i\omega U_{\mathrm{reg},j}(p)=-i\omega C_{2j}e^{ik_{j}p},
\end{align*}
y se satisface las relaciones entre presiones y velocidades siguientes:
\begin{align*}
\Pi_{\mathrm{pro},j}(p)&=Z_{j}V _{\mathrm{pro},j}(p)=-i\omega Z_{j} U_{\mathrm{pro},j}(p),\\
\Pi_{\mathrm{reg},j}(p)&=-Z_{j}V _{\mathrm{reg},j}(p)=i\omega Z_{j} U_{\mathrm{reg},j}(p)
\end{align*}
donde $Z_{j}$ denota la impedancia característica del medio $j$.

En consecuencia, si utilizamos este hecho y la definición de intensidad acústica (producto de presión por velocidad),
la intensidad acústica de la onda plana progresiva
$i_{\mathrm{pro},j}=\mathrm{Re}(e^{-i\omega t}I_j\mathbf{e}_1)$ en el medio $j$ se puede escribir como
\begin{align}
I_j(p):&=(i_{\mathrm{pro},j})_{\text{av}}(p)\cdot\mathbf{e}_1
=-\frac{\omega}{2}\mbox{Im}\left(\Pi_{\mathrm{pro},j}(p)
\overline{U }_{\mathrm{pro},j}(p)\right) \\
&=\frac{\omega^2}{2}\mathrm{Re}\left(Z_j\left|U_{\mathrm{pro},j}(p)\right|^2\right)
=\frac{\omega^2\mathrm{Re}(Z_j)}{2}|C_{2j}|^2|e^{ik_{j}p}|^2,\ j=1,2,\ldots,n.
\end{align}
Si el medio $j$ es un fluido compresible, tanto la impedancia $Z_j$
como el número de onda $k_{j}=\frac{\omega}{c_{j}}$
son reales y por tanto,
\begin{equation*}
I_j(p)=\frac{\omega^2 Z_j}{2}|C_{2j}|^2.
\end{equation*}
Volviendo al caso general y teniendo en cuenta la expresión de la intesidad,
el coeficiente de transmisión entre los medios $j$ y $j+1$
se define como
\begin{equation}
\tau_j=\frac{I_{j+1}(a_j)}{I_j(a_j)}=
\frac{|C_{2j+2}|^2|e^{ik_{j+1}a_j}|^2\mbox{Re}(Z_{j+1})}
{|C_{2j}|^2|e^{ik_{j}a_j}|^2\mbox{Re}(Z_{j})},\ j=1,2,\ldots,n-1.
\end{equation}
También puede ser de interés saber cuál es el coeficiente de transmisión entre el
medio del que parte la onda que excita la multicapa y el medio $n$,
definido como $\tau=\tau_1\ldots\tau_{n-1}$.

Si suponemos que todos los medios son no disipativos obtenemos
\begin{equation*}
\tau=\tau_1\cdots\tau_{n-1}=\prod_{j=1}^{n-1}\frac{I_{j+1}(a_j)}{I_j(a_j)}
=\prod_{j=1}^{n-1}\frac{Z_{j+1}|C_{2j+2}|^2}{Z_j|C_{2j}|^2}
= \frac{Z_{n}|C_{2n}|^2}{Z_{1}|C_{2}|^2},
\end{equation*}
lo que da una valoración de la pérdida de energía
que sufrió la onda al viajar a través de la multicapa.

De la misma forma, también podemos escribir el valor eficaz
de presión sonora en cada medio en función de las amplitudes
de las ondas planas, para cada $j=1,2,\ldots,n$, como
\begin{align*}
(\pi_{\mathrm{rms},j})(p)&=\frac{1}{\sqrt{2}}|\Pi_j(p)|=\frac{\omega}{\sqrt{2}}|Z_j|
|-C_{2j-1}e^{-ik_{j}p}+C_{2j}e^{ik_{j}p}|.
\end{align*}

Asociadas a estas medidas de valoración del sonido, es usual
definir la impedancia de entrada y salida en la interfaz $j$:

### Impedancias de entrada y salida
Dado un medio multicapa formado por $n$ medios,
la **impedancia de entrada** en la interfaz $j$-ésima de la multicapa
se define por
\begin{equation*}
Z^{\mathrm{in}}_{j}=\frac{\Pi_j(a_j)}{V _j(a_j)},\ j=1,2,\ldots,n-1.
\end{equation*}
mientras que la **impedancia de salida** en la interfaz $j$-ésima de la multicapa viene
dada por la expresión
\begin{equation*}
Z^{\mathrm{out}}_{j}=\frac{\Pi_{j+1}(a_j)}{V _{j+1}(a_j)},\ j=1,2,\ldots,n-1.
\end{equation*}

Estas impedancias pueden escribirse como
\begin{eqnarray*}
Z^{\mathrm{in}}_{j}&=&Z_j\frac{-C_{2j-1}e^{-ik_{j}a_j}+C_{2j}e^{ik_{j}a_j}}
{C_{2j-1}e^{-ik_{j}a_j}+C_{2j}e^{ik_{j}a_j}}, \\
Z^{\mathrm{out}}_{j}&=&Z_{j+1}\frac{-C_{2j+1}e^{-ik_{j+1}a_j}+C_{2j+2}e^{ik_{j+1}a_j}}
{C_{2j+1}e^{-ik_{j+1}a_j}+C_{2j+2}e^{ik_{j+1}a_j}}.
\end{eqnarray*}
Si la condición entre los dos medios es de contacto, se obtiene inmediatamente
$$
Z^{\mathrm{in}}_{j}=\frac{\Pi_j(a_j)}{V_{j}(a_j)}
=\frac{\Pi_{j+1}(a_j)}{V_{j}(a_{j+1})}=Z^{\mathrm{out}}_{j}.
$$

Es habitual definir la impedancia de entrada del medio multicapa como $Z_I=Z^{\mathrm{in}}_{1}$
y la impedancia de salida del medio multicapa como $Z_T=Z^{\mathrm{out}}_{n-1}$.

### Coeficientes de reflexión, absorción y transmisión

El nivel de presión sonora, denotado por SPL o bien por $L_{\pi}$, se define como el cociente entre el
valor eficaz de la presión sonora $\pi_{\text{rms}}$
y el valor de referencia o umbral de presión $\pi_{\text{ref}}=2\times 10^{-5}$ Pa,
\begin{equation}
\text{SPL}=20\log_{10}\left(\frac{\pi_{\text{rms}}}{\pi_{\text{ref}}}\right).
\end{equation}

En el régimen armónico, los coeficientes de reflexión y transmisión
asociados a la presión son los números complejos $R$ y $T$ definidos
respectivamente como
$$
\begin{array}{ccc}
R&=&\frac{\Pi_{\text{refl}}}{\Pi_{\text{inc}}},\\
T&=&\frac{\Pi_{\text{tran}}}{\Pi_{\text{inc}}},
\end{array}
$$
donde $\Pi_{\text{inc}}$, $\Pi_{\text{refl}}$ y $\Pi_{\text{tran}}$ son las amplitudes de las ondas planas
asociadas al campo de presiones incidente, reflejada y trasmitida, respectivamente, en el dominio
de la frecuencia.

Para ver, de una forma más clara, en qué medida una superficie
es capaz de amortiguar la propagación acústica, se introduce
el coeficiente de absorción. Este coeficiente está
íntimamente relacionado con los coeficientes de reflexión ya
que se define como
\begin{equation*}
a=1-|R|^2,
\end{equation*}
lo que implica que $0\le a\le 1$. En general, la absorción tiene
en cuenta tanto la energía transmitida como la energía
disipada al propagarse la onda a través de los diferentes medios.

Aunque no incluya en su denominación la palabra nivel, se
trata de uno de ellos y por tanto se mide en decibelios. La
pérdida de transmisión se define como el logaritmo del inverso
del coeficiente de transmisión $\tau$ asociado a la intensidad,
\begin{equation*}
\text{TL}=10\log_{10}\frac{1}{\tau}.
\end{equation*}

## Propagación de ondas planas con incidencia normal a través de dos fluidos de diferentes características

Obviamente la propagación a través de una interfaz de
contacto que separa dos fluidos idénticos no implica reflexión alguna, puesto que
las impedancias de entrada y salida a la interfaz son iguales a
la impedancia característica de ambos medios.
Sin embargo, si los medios poseen diferentes propiedades pueden aparecer fenómenos de reflexión.
Las ecuaciones que gobiernan de transmisión de ondas planas en este caso son
$$
\left\{\begin{array}{l}
U_1(a)=U_2(a),\\
\rho_1 c_{1}^{2}\frac{d U_1}{d p}(a)=\rho_2 c_{2}^2\frac{d U_2}{d p}(a),
\end{array}\right.
$$

```python
import numpy as np
# Constantes físicas del problem
rho1 = 1 # densidad de masa medio 1
rho2 = 2 # densidad de masa medio 2
c1 = 340 # velocidad del sonido medio 1
c2 = 480 # velocidad del medio 2
F = 1 # Amplitud incidente

def compute_TMM(rho1,rho2,c1,c2,F,omega):
    k1 = omega/c1
    k2 = omega/c2
    A = np.array([[0,1,0,0,], [1,1,-1,-1],
                  [rho1*c1**2*(-1j*k1),rho1*c1**2*(1j*k1),-rho2*c2**2*(-1j*k2),-rho2*c2**2*(1j*k2)],
                  [0,0,1,0]])
    rhs = np.array([F,0,0,0])
    return np.linalg.solve(A, rhs)

# Barrido en frecuencia para el coeficiente de absorción
nfreq=100
freq = np.linspace(5,1000,nfreq) # Frecuencia [hz]
absorcion = np.zeros(nfreq)
for i,fi in enumerate(freq):
    Ci = compute_TMM(rho1,rho2,c1,c2,F,2*np.pi*fi)
    absorcion[i]=1-np.abs(Ci[0])**2

# Representación gráfica
import matplotlib as mp
import matplotlib.pyplot as plt
%matplotlib inline
plt.plot(freq, absorcion, label='absorción')
plt.xlabel('Frecuencia [Hz]')
plt.ylabel('Absorción')
plt.legend()
plt.show()
```

## Propagación de ondas planas con incidencia normal a través de dos fluidos de las mismas características pero separados por una placa rígida

Obviamente la propagación a través de una interfaz de
contacto que separa dos fluidos idénticos no implica reflexión alguna, puesto que
las impedancias de entrada y salida a la interfaz son iguales a
la impedancia característica de ambos medios.
Sin embargo, la inclusión de una placa rígida entre los
dos medios modifica esta situación (véase la Figura más bajo).

Las ecuaciones que gobiernan de transmisión de ondas planas en este caso son
$$
\left\{\begin{array}{l}
U_1(a)=U_2(a),\\
-\rho_1 c_{1}^{2}\frac{d U_1}{d p}(a)=-\rho_2 c_{2}^2\frac{d U_2}{d p}(a)+(\rho_{\mathrm{p}}\omega^2+i\omega s-r)U_2(a),
\end{array}\right.
$$
donde $\rho_{\mathrm{p}}$, $s$ y $r$ son la densidad de masa superficial, el coeficiente de amortiguamiento y el coeficiente de elasticidad de la placa rígida.

```python
import numpy as np
# Constantes físicas del problema
rho1 = 1 # densidad de masa medio 1
rho2 = 1 # densidad de masa medio 2
c1 = 340 # velocidad del sonido medio 1
c2 = 340 # velocidad del medio 2
F = 1 # Amplitud incidente

# Constantes físicas de la placa rígida
rhop = 0.7 # densidad de masa superficial
s = 0.2 # coeficiente de amortiguamiento
r = 10 # coeficiente de elasticidad

def compute_TMM(rho1,rho2,c1,c2,rhop,s,r,F,omega):
    k1 = omega/c1
    k2 = omega/c2
    Zw = rhop*omega**2+1j*omega*s-r # impedancia mecánica de la placa
    A = np.array([[0,1,0,0,], [1,1,-1,-1],
                  [-rho1*c1**2*(-1j*k1),-rho1*c1**2*(1j*k1),+rho2*c2**2*(-1j*k2)-Zw,+rho2*c2**2*(1j*k2)-Zw],
                  [0,0,1,0]])
    rhs = np.array([F,0,0,0])
    return np.linalg.solve(A, rhs)

# Barrido en frecuencia para el coeficiente de transmisión
nfreq=100
freq = np.linspace(5,1000,nfreq) # Frecuencia [hz]
transmision = np.zeros(nfreq)
for i,fi in enumerate(freq):
    Ci = compute_TMM(rho1,rho2,c1,c2,rhop,s,r,F,2*np.pi*fi)
    transmision[i]=np.abs(Ci[3])

# Representación gráfica
import matplotlib as mp
import matplotlib.pyplot as plt
%matplotlib inline
plt.plot(freq, transmision, label='transmisión')
plt.xlabel('Frecuencia [Hz]')
plt.ylabel('Transmisión')
plt.legend()
plt.show()
```

## Propagación de ondas planas con incidencia  normal a través de dos medios (fluidos o medios porosos) con una interfaz de contacto común y uno de ellos en contacto con una pared rígida

Si suponemos que el segundo medio es el que está en contacto
con la pared rígida a una distancia $a$ de la interfaz común que
situamos en $p=0$ (véase la Figura más abajo),

las condiciones de transmisión que debemos imponer en las dos interfaces son
$$
\left\{\begin{array}{l}
U_1(0)=U_2(0),\\
-\rho_1 c_{1}^{2}\frac{d U_1}{d p}(0)=-\rho_2 c_{2}^2\frac{d U_2}{d p}(0),\\
U_2(a)=0.
\end{array}\right.
$$

Si suponemos que la amplitud de la onda incidente es igual a la unidad,
el sistema lineal a resolver es
$$
\left\{\begin{array}{ccc}
C_2&=&1,\\
C_1+C_2&=&C_3+C_4, \\
Z_1 (-C_1+C_2)&=&Z_2 (-C_3+C_4),\\
C_3e^{-ik_{2}a}+C_4e^{ik_{2}a}&=&0,
\end{array}\right.
$$
donde $Z_1$ y $Z_2$ son las impedancias características del medio $1$ y $2$, respectivamente.

```python
import numpy as np
# Constantes físicas del problem
rho1 = 1 # densidad de masa medio 1
rho2 = 2 # densidad de masa medio 2
c1 = 340 # velocidad del sonido medio 1
c2 = 480 # velocidad del medio 2
F = 1 # Amplitud incidente
a = 0.1 # espesor capa 2

def compute_TMM(rho1,rho2,c1,c2,a,F,omega):
    k1 = omega/c1
    k2 = omega/c2
    A = np.array([[0,1,0,0,], [1,1,-1,-1],
                  [rho1*c1**2*(-1j*k1),rho1*c1**2*(1j*k1),-rho2*c2**2*(-1j*k2),-rho2*c2**2*(1j*k2)],
                  [0,0,np.exp(-1j*k2*a),np.exp(1j*k2*a)]])
    rhs = np.array([F,0,0,0])
    return np.linalg.solve(A, rhs)

# Barrido en frecuencia para la impedancia de entrada
nfreq=100
freq = np.linspace(50,1000,nfreq) # Frecuencia [hz]
Zinput = np.zeros(nfreq, dtype=np.complex)
Zoutput = np.zeros(nfreq)
for i,fi in enumerate(freq):
    Ci = compute_TMM(rho1,rho2,c1,c2,a,F,2*np.pi*fi)
    Zinput[i]=rho1*c1*(Ci[0]-Ci[1])/(Ci[0]+Ci[1])

# Representación gráfica
import matplotlib as mp
import matplotlib.pyplot as plt
%matplotlib inline
plt.plot(freq, Zinput.real, label='Real')
plt.plot(freq, Zinput.imag, label='Imag')
plt.xlabel('Frecuencia [Hz]')
plt.ylabel('Impedancia entrada')
plt.legend()
plt.show()
```

### Ejercicio

Considera un sistema multicapa formado por cuatro medios:
- Medio 1: Fluido $\rho_{\mathrm{F}}=1\,$kg/m$^3$, $c_{\mathrm{F}}=340\,$m/s, no acotado cuando $p_{1}\to-\infty$.
- Medio 2: Fluido $\rho_{\mathrm{F}}=1\,$kg/m$^3$, $c_{\mathrm{F}}=340\,$m/s y de espesor $0.3\,$m.
- Medio 3: Sólido elástico $\rho_{\mathrm{s}}=7700\,$kg/m$^3$, $c_{\mathrm{s}}=\sqrt{(\lambda+2\mu)/\rho_{\mathrm{s}}}$ donce $\lambda=1.24\times 10^{11}\,$Pa, $\mu=5.33\times 10^{10}\,$Pa y de espesor $0.02\,$m.
- Medio 4: Fluido $\rho_{\mathrm{F}}=1\,$kg/m$^3$, $c_{\mathrm{F}}=340\,$m/s, no acotado cuando $p_{1}\to+\infty$.

La interfaz de acople entre el medio 1 y el medio 2 posee un panel visco-elástico
\begin{eqnarray*}
\Pi^{-}(a)=\Pi^{+}(a),\\
U^{+}(a)=U^{-}(a)-i\omega Z(\omega)U^{-}(a),
\end{eqnarray*}
donde $Z(\omega)$ es la impedancia del velo definida por $Z(\omega)=\beta+i\frac {\alpha}{\omega}$,
con propiedades $\alpha=5\times 10^6\,$N/m$^3$ y $\beta=50\,$Ns/m$^3$. La interfaz
entre el medio 2 y el 3 es de contacto perfecto y entre el medio 3 y el medio 4
existe un velo poroso
\begin{eqnarray*}
U^{-}(a)=U^{+}(a),\\
-\Pi^{+}(a)=-\Pi^{-}(a)-i\omega Z(\omega)U^{-}(a),
\end{eqnarray*}
donde $Z(\omega)$ es la impedancia del velo definida por $Z(\omega)=\beta+i\frac {\alpha}{\omega}$,
con las mismos parámetros $\alpha$ y $\beta$ que el material
visco-elástico. Si la frecuencia se fija a $400\,$Hz y se supone que una onda plana
progresiva de amplitud $1\,$m en desplazamientos incide desde el medio 1:

- Representa gráficamente  el nivel TL y el coeficiente de absorción del medio multicapa entre $5$ y $1000$ Hz.
- Decide de forma razonada si existen frecuencias de resonancia reales para este problema de propagación de ondas.

```python
## TU CÓDIGO AQUÍ
```


# Project 2, Coupled cluster studies of electronic systems. Deadline May 31

**[Computational Physics II FYS4411/FYS9411](http://www.uio.no/studier/emner/matnat/fys/FYS4411/index-eng.html)**, Department of Physics, University of Oslo, Norway

Date: **Jan 16, 2019**

Copyright 1999-2019, [Computational Physics II FYS4411/FYS9411](http://www.uio.no/studier/emner/matnat/fys/FYS4411/index-eng.html). Released under CC Attribution-NonCommercial 4.0 license

## Introduction

The aim of this project is to use the coupled cluster method
method to evaluate
the ground state energy of
quantum dots with $N=2$, $N=6$, $N=12$ and $N=20$ electrons. These are so-called closed shell systems.

## Theoretical background and description of the physical system

We consider a system of electrons confined in a pure two-dimensional
isotropic harmonic oscillator potential, with an idealized  total Hamiltonian given by

$$
\begin{equation}
\label{eq:finalH} \tag{1}
\hat{H}=\sum_{i=1}^{N} \left(  -\frac{1}{2} \nabla_i^2 + \frac{1}{2} \omega^2r_i^2  \right)+\sum_{i<j}\frac{1}{r_{ij}},
\end{equation}
$$

where natural units ($\hbar=c=e=m_e=1$) are used and all energies are in so-called atomic units a.u. We will study systems of many electrons $N$ as functions of the oscillator frequency  $\omega$ using the above Hamiltonian.  The Hamiltonian includes a standard harmonic oscillator part

$$
\hat{H}_0=\sum_{i=1}^{N} \left(  -\frac{1}{2} \nabla_i^2 + \frac{1}{2} \omega^2r_i^2  \right),
$$

and the repulsive interaction between two electrons given by

$$
\hat{H}_1=\sum_{i<j}\frac{1}{r_{ij}},
$$

with the distance between electrons given by $r_{ij}=\vert \boldsymbol{r}_1-\boldsymbol{r}_2\vert$. We define the
modulus of the positions of the electrons (for a given electron $i$) as $r_i = \sqrt{r_{i_x}^2+r_{i_y}^2}$.

The aim of this project is to develop a coupled cluster doubles (CCD) code, where $2p-2h$ excitations are included only.

We will start with a two-electron problem and compare our results to those of Taut, see reference [1] below.

The ansatz for the ground state  is given by

$$
\vert \Psi_0\rangle = \vert \Psi_{CC}\rangle = e^{\hat{T}} \vert
     \Phi_0\rangle = \left( \sum_{n=1}^{N} \frac{1}{n!} \hat{T}^n
     \right) \vert \Phi_0\rangle,
$$

where $N$ represents the maximum number of particle-hole excitations
  and $\hat{T}$ is the cluster operator defined as

$$
\begin{align*}
              \hat{T} &= \hat{T}_1 + \hat{T}_2 + \ldots + \hat{T}_N
              \\ \hat{T}_n &= \left(\frac{1}{n!}\right)^2
              \sum_{\substack{ i_1,i_2,\ldots i_n \\ a_1,a_2,\ldots
                  a_n}} t_{i_1i_2\ldots i_n}^{a_1a_2\ldots a_n}
              a_{a_1}^\dagger a_{a_2}^\dagger \ldots a_{a_n}^\dagger
              a_{i_n} \ldots a_{i_2} a_{i_1}.
          \end{align*}
$$

The energy is given by

$$
E_{\mathrm{CC}} = \langle\Phi_0\vert \overline{H}\vert
          \Phi_0\rangle,
$$

where $\overline{H}$ is a similarity transformed Hamiltonian

$$
\begin{align*}
          \overline{H}&= e^{-\hat{T}} \hat{H}_N e^{\hat{T}}
          \\ \hat{H}_N &= \hat{H} - \langle\Phi_0\vert \hat{H} \vert
          \Phi_0\rangle.
      \end{align*}
$$

The coupled cluster energy is a function of the unknown cluster
      amplitudes $t_{i_1i_2\ldots i_n}^{a_1a_2\ldots a_n}$, given by
      the solutions to the amplitude equations

$$
\begin{equation}\label{eq:amplitudeeq} \tag{2}
          0 = \langle\Phi_{i_1 \ldots i_n}^{a_1 \ldots a_n}\vert
          \overline{H}\vert \Phi_0\rangle.
      \end{equation}
$$

In order to set up the above equations,
the similarity transformed Hamiltonian $\overline{H}$ is expanded
  using the Baker-Campbell-Hausdorff expression,

$$
\begin{equation}\label{eq:bch} \tag{3}
          \overline{H}= \hat{H}_N + \left[ \hat{H}_N, \hat{T} \right]
          + \frac{1}{2} \left[\left[ \hat{H}_N, \hat{T} \right],
            \hat{T}\right] + \ldots + \frac{1}{n!} \left[
            \ldots \left[ \hat{H}_N, \hat{T} \right], \ldots \hat{T}
            \right] +\dots
      \end{equation}
$$

and simplified using the connected cluster theorem

$$
\overline{H}= \hat{H}_N + \left( \hat{H}_N \hat{T}\right)_c
          + \frac{1}{2} \left( \hat{H}_N \hat{T}^2\right)_c + \dots +
          \frac{1}{n!} \left( \hat{H}_N \hat{T}^n\right)_c +\dots
$$

We will discuss parts of the the derivation below.

  We will now approximate the cluster operator $\hat{T}$ to include
  only $2p-2h$ correlations. This leads to the so-called CCD
  approximation, that is

$$
\hat{T}\approx
  \hat{T}_2=\frac{1}{4}\sum_{abij}t_{ij}^{ab}a^{\dagger}_aa^{\dagger}_ba_ja_i,
$$

meaning that we have

$$
\vert \Psi_0 \rangle \approx \vert \Psi_{CCD} \rangle =
  \exp{\left(\hat{T}_2\right)}\vert \Phi_0\rangle.
$$

Inserting these equations in the expression for the computation of
  the energy we have, with a Hamiltonian defined with respect to a
  general reference vacuum

$$
\hat{H}=\hat{H}_N+E_{\mathrm{ref}},
$$

with

$$
\hat{H}_N=\sum_{pq}\langle p \vert \hat{f} \vert q \rangle
  a^{\dagger}_pa_q + \frac{1}{4}\sum_{pqrs}\langle pq \vert \hat{v}
  \vert rs \rangle a^{\dagger}_pa^{\dagger}_qa_sa_r,
$$

we obtain that the energy can be written as

$$
\langle \Phi_0 \vert
  \exp{\left(-\hat{T}_2\right)}\hat{H}_N\exp{\left(\hat{T}_2\right)}\vert
  \Phi_0\rangle = \langle \Phi_0 \vert \hat{H}_N(1+\hat{T}_2)\vert
  \Phi_0\rangle = E_{CCD}.
$$

This quantity becomes

$$
E_{CCD}=E_{\mathrm{ref}}+\frac{1}{4}\sum_{abij}\langle ij \vert
  \hat{v} \vert ab \rangle t_{ij}^{ab},
$$

where the latter is the correlation energy from this level of
  approximation of coupled cluster  theory.  Similarly, the expression for the
  amplitudes reads

$$
\langle \Phi_{ij}^{ab} \vert
  \exp{\left(-\hat{T}_2\right)}\hat{H}_N\exp{\left(\hat{T}_2\right)}\vert
  \Phi_0\rangle = 0.
$$

These equations can be reduced to (after several applications of
  Wick's theorem), for all $i > j$ and all $a > b$,

$$
0 = \langle ab \vert \hat{v} \vert ij \rangle +
  \left(\epsilon_a+\epsilon_b-\epsilon_i-\epsilon_j\right)t_{ij}^{ab}+\frac{1}{2}\sum_{cd} \langle ab \vert \hat{v} \vert
  cd \rangle t_{ij}^{cd}+\frac{1}{2}\sum_{kl} \langle kl \vert \hat{v}
  \vert ij \rangle t_{kl}^{ab}+\hat{P}(ij\vert ab)\sum_{kc} \langle kb
  \vert \hat{v} \vert cj \rangle t_{ik}^{ac}  \nonumber
$$

$$
\begin{equation}  +\frac{1}{4}\sum_{klcd} \langle kl \vert \hat{v} \vert cd \rangle
  t_{ij}^{cd}t_{kl}^{ab}+\hat{P}(ij)\sum_{klcd} \langle kl \vert
  \hat{v} \vert cd \rangle t_{ik}^{ac}t_{jl}^{bd}-\frac{1}{2}\hat{P}(ij)\sum_{klcd} \langle kl \vert \hat{v} \vert
  cd \rangle t_{ik}^{dc}t_{lj}^{ab}-\frac{1}{2}\hat{P}(ab)\sum_{klcd}
  \langle kl \vert \hat{v} \vert cd \rangle t_{lk}^{ac}t_{ij}^{db},
  \label{eq:ccd} \tag{4}
  \end{equation}
$$

where we have defined

$$
\hat{P}\left(ab\right)= 1-\hat{P}_{ab},
$$

where $\hat{P}_{ab}$ interchanges two particles occupying the
  quantum numbers $a$ and $b$.  The operator $\hat{P}(ij\vert ab)$ is
  defined as

$$
\hat{P}(ij\vert ab) = (1-\hat{P}_{ij})(1-\hat{P}_{ab}).
$$

The single-particle energies $\epsilon_p$ are normally taken to be Hartree-Fock single-particle energies.
  Recall also that the unknown amplitudes $t_{ij}^{ab}$ represent
  anti-symmetrized matrix elements, meaning that they obey the same
  symmetry relations as the two-body interaction, that is

$$
t_{ij}^{ab}=-t_{ji}^{ab}=-t_{ij}^{ba}=t_{ji}^{ba}.
$$

The two-body matrix elements are also anti-symmetrized, meaning that

$$
\langle ab \vert \hat{v} \vert ij \rangle = -\langle ab \vert
  \hat{v} \vert ji \rangle= -\langle ba \vert \hat{v} \vert ij
  \rangle=\langle ba \vert \hat{v} \vert ji \rangle.
$$

The non-linear equations for the unknown amplitudes $t_{ij}^{ab}$
  are solved iteratively.

In order to develop a program, chapter 8 of the recent [Lecture Notes in Physics (volume 936)](http://www.springer.com/us/book/9783319533353) is highly recommended as literature.
All material is available from the [source site](https://github.com/ManyBodyPhysics/LectureNotesPhysics/blob/master/doc/src/lnp.pdf). Example of CCD codes are available from the [program site](https://github.com/ManyBodyPhysics/LectureNotesPhysics/tree/master/Programs/Chapter8-programs/cpp/CCD). These can be used to benchmark your own program.

### Project 2 a):

We will use our Hartree-Fock basis from project 1 to define matrix elements and the single-particle energies to be used in the CCD equations. The Hartree-Fock basis defines the so-called reference energy

$$
\begin{equation}
  E_{\mathrm{ref}} = \sum_{i\le F} \sum_{\alpha\beta}
  C^*_{i\alpha}C_{i\beta}\langle \alpha | h | \beta \rangle +
  \frac{1}{2}\sum_{ij\le F}\sum_{{\alpha\beta\gamma\delta}}
  C^*_{i\alpha}C^*_{j\beta}C_{i\gamma}C_{j\delta}\langle
  \alpha\beta|\hat{v}|\gamma\delta\rangle.
\label{_auto1} \tag{5}
\end{equation}
$$

You will need to transform the matrix elements from the harmonic oscillator basis to the Hartree-Fock basis.
The first step is to program

$$
\begin{equation}
  \langle pq \vert \hat{v} \vert rs\rangle_{AS}=
  \sum_{{\alpha\beta\gamma\delta}}
  C^*_{p\alpha}C^*_{q\beta}C_{r\gamma}C_{s\delta}\langle
  \alpha\beta|\hat{v}|\gamma\delta\rangle_{AS},
\label{_auto2} \tag{6}
\end{equation}
$$

where the coefficients are those from the last Hartree-Fock iteration and the matrix elements are all anti-symmetrized.
You can extend your Hartree-Fock program to write out these matrix elements after the last Hartree-Fock iteration.
Make sure that your matrix elements are structured according to conserved quantum numbers, avoiding thereby the write out of many zeros.

To test that your matrix elements are set up correctly, when you read in these matrix elements in the CCD code, make sure that the reference energy from your Hartree-Fock calculations are reproduced.

### Project 2 b):

Set up a code which solves the CCD equation by encoding the equations as they stand, that is follow the mathematical expressions and perform the sums over all single-particle states.  Compute the energy of the two-electron systems using
all single-particle states that were needed in order to obtain the Hartree-Fock limit. Compare these with Taut's results for $\omega=1$ a.u.  Since you do not include singles you will not get the exact result. If you wish to include singles, you will able to obtain the exact results in a basis with at least ten major oscillator shells.
Perform also calculations with $N=6$, $N=12$ and $N=20$ electrons and compare with reference [2] of Pedersen et al below.

### Project 2 c):

The next step consists in rewriting the equations in terms of matrix-matrix multiplications and subdividing
the matrix elements and operations in terms of two-particle configuration that conserve total spin projection and projection of the orbital momentum. Rewrite also the equations in terms of so-called intermediates, as detailed
in section 8.7 of [Lietz et al](https://github.com/ManyBodyPhysics/LectureNotesPhysics/blob/master/doc/src/lnp.pdf).
This section gives a detailed description on how to build a coupled cluster code and is highly recommended.

Rerun your calculations for $=2$, $N=6$, $N=12$ and $N=20$ electrons using your optimal Hartree-Fock basis. Make sure your results from 2b) stay the same.

Calculate as well ground state energies for $\omega=0.5$ and $\omega=0.1$. Try to compare with eventual variational
Monte Carlo results from other students, if possible.

### Project 2 d):

The final step is to parallelize your CCD code using either OpenMP or MPI and do a performance analysis. Use the $N=6$ case. Make a performance analysis by timing your serial code
with and without vectorization. Perform several runs and compute an average timing analysis
with and without vectorization. Comment your results.

Compare thereafter your serial code(s)  with the speedup you get by parallelizing your code, running either OpenMP or MPI or both.
Do you get a near $100\%$ speedup with the parallel version? Comment again your results and perform timing benchmarks several times in order
to extract  an average performance time.

### Literature

1. M. Taut, Phys. Rev. A **48**, 3561 - 3566 (1993).

2. M. L. Pedersen, G. Hagen, M. Hjorth-Jensen, S. Kvaal,  and F. Pederiva, Phys. Rev. B **84**, 115302 (2011)

## Introduction to numerical projects

Here follows a brief recipe and recommendation on how to write a report for each
project.

  * Give a short description of the nature of the problem and the eventual  numerical methods you have used.

  * Describe the algorithm you have used and/or developed. Here you may find it convenient to use pseudocoding. In many cases you can describe the algorithm in the program itself.

  * Include the source code of your program. Comment your program properly.

  * If possible, try to find analytic solutions, or known limits in order to test your program when developing the code.

  * Include your results either in figure form or in a table. Remember to label your results. All tables and figures should have relevant captions and labels on the axes.

  * Try to evaluate the reliabilty and numerical stability/precision of your results. If possible, include a qualitative and/or quantitative discussion of the numerical stability, eventual loss of precision etc.

  * Try to give an interpretation of you results in your answers to  the problems.

  * Critique: if possible include your comments and reflections about the  exercise, whether you felt you learnt something, ideas for improvements and  other thoughts you've made when solving the exercise. We wish to keep this course at the interactive level and your comments can help us improve it.

  * Try to establish a practice where you log your work at the  computerlab. You may find such a logbook very handy at later stages in your work, especially when you don't properly remember  what a previous test version  of your program did. Here you could also record  the time spent on solving the exercise, various algorithms you may have tested or other topics which you feel worthy of mentioning.

## Format for electronic delivery of report and programs

The preferred format for the report is a PDF file. You can also use DOC or postscript formats or as an ipython notebook file.  As programming language we prefer that you choose between C/C++, Fortran2008 or Python. The following prescription should be followed when preparing the report:

  * Use Devilry to hand in your projects, log in  at  <http://devilry.ifi.uio.no> with your normal UiO username and password.

  * Upload **only** the report file!  For the source code file(s) you have developed please provide us with your link to your github domain.  The report file should include all of your discussions and a list of the codes you have developed.  The full version of the codes should be in your github repository.

  * In your github repository, please include a folder which contains selected results. These can be in the form of output from your code for a selected set of runs and input parameters.

  * Still in your github make a folder where you place your codes.

  * In this and all later projects, you should include tests (for example unit tests) of your code(s).

  * Comments  from us on your projects, approval or not, corrections to be made  etc can be found under your Devilry domain and are only visible to you and the teachers of the course.

Finally,
we encourage you to work two and two together. Optimal working groups consist of
2-3 students. You can then hand in a common report.